New home UI
This commit is contained in:
parent
842667cdee
commit
71a314b0a0
|
@ -68,15 +68,15 @@ dependencies {
|
|||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation "com.github.bumptech.glide:glide:4.12.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
|
||||
def exoplayer_version = "2.16.1"
|
||||
def exoplayer_version = "2.17.0"
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
||||
|
||||
implementation "androidx.concurrent:concurrent-futures:1.1.0"
|
||||
|
||||
def camerax_version = "1.1.0-beta01"
|
||||
def camerax_version = "1.1.0-beta02"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
implementation "androidx.camera:camera-view:$camerax_version"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 1da2407a614f17a3c64d14ee34fb41e081db9a71
|
||||
Subproject commit 89966b1aaef93ac842f2240451ebfd50dd2bbce9
|
|
@ -31,61 +31,30 @@
|
|||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseTheme">
|
||||
<activity
|
||||
android:name=".CameraActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".explorers.ExplorerActivity" />
|
||||
<activity android:name=".explorers.ExplorerActivityPick" />
|
||||
<activity android:name=".explorers.ExplorerActivityDrop" />
|
||||
<activity
|
||||
android:name=".OpenActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/share_menu_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".CreateActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".ChangePasswordActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="nosensor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".file_viewers.ImageViewer"
|
||||
android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
|
||||
<activity
|
||||
android:name=".file_viewers.VideoPlayer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.PdfViewer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.AudioPlayer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.TextEditor"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".SettingsActivity" android:label="@string/title_activity_settings"/>
|
||||
<activity android:name=".add_volume.AddVolumeActivity" android:windowSoftInputMode="adjustResize"/>
|
||||
<activity android:name=".ChangePasswordActivity" android:windowSoftInputMode="adjustResize"/>
|
||||
<activity android:name=".explorers.ExplorerActivity"/>
|
||||
<activity android:name=".explorers.ExplorerActivityPick"/>
|
||||
<activity android:name=".explorers.ExplorerActivityDrop"/>
|
||||
<activity android:name=".file_viewers.ImageViewer" android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
|
||||
<activity android:name=".file_viewers.VideoPlayer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.PdfViewer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
|
||||
|
||||
<service android:name=".file_operations.FileOperationService" android:exported="false"/>
|
||||
|
||||
|
|
|
@ -8,13 +8,13 @@ import androidx.preference.PreferenceManager
|
|||
|
||||
open class BaseActivity: AppCompatActivity() {
|
||||
protected lateinit var sharedPrefs: SharedPreferences
|
||||
protected lateinit var themeValue: String
|
||||
protected var shouldCheckTheme = true
|
||||
lateinit var themeValue: String
|
||||
private var shouldCheckTheme = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (shouldCheckTheme) {
|
||||
themeValue = sharedPrefs.getString("theme", "dark_green")!!
|
||||
themeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!!
|
||||
when (themeValue) {
|
||||
"black_green" -> setTheme(R.style.BlackGreen)
|
||||
"dark_red" -> setTheme(R.style.DarkRed)
|
||||
|
|
|
@ -91,6 +91,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
|||
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
binding = ActivityCameraBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.hide()
|
||||
gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
|
||||
outputDirectory = intent.getStringExtra("path")!!
|
||||
|
||||
|
|
|
@ -1,195 +1,161 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
||||
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.WidgetUtil
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class ChangePasswordActivity : VolumeActionActivity() {
|
||||
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
||||
class ChangePasswordActivity: BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityChangePasswordBinding
|
||||
private lateinit var volume: Volume
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private var fingerprintProtector: FingerprintProtector? = null
|
||||
private var usfFingerprint: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
volume = intent.getParcelableExtra("volume")!!
|
||||
binding = ActivityChangePasswordBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupLayout()
|
||||
setupFingerprintStuff()
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, themeValue, volumeDatabase)
|
||||
if (savedVolumesAdapter.count > 0){
|
||||
binding.savedPathListview.adapter = savedVolumesAdapter
|
||||
binding.savedPathListview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
|
||||
val volume = savedVolumesAdapter.getItem(position)
|
||||
currentVolumeName = volume.name
|
||||
if (volume.isHidden){
|
||||
switchHiddenVolume.isChecked = true
|
||||
editVolumeName.setText(currentVolumeName)
|
||||
} else {
|
||||
switchHiddenVolume.isChecked = false
|
||||
editVolumePath.setText(currentVolumeName)
|
||||
}
|
||||
onClickSwitchHiddenVolume()
|
||||
}
|
||||
} else {
|
||||
WidgetUtil.hideWithPadding(binding.savedPathListview)
|
||||
}
|
||||
val textWatcher = object: TextWatcher{
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
if (volumeDatabase.isVolumeSaved(s.toString())){
|
||||
checkboxRememberPath.isEnabled = false
|
||||
checkboxRememberPath.isChecked = true
|
||||
binding.editOldPassword.apply {
|
||||
if (volumeDatabase.isHashSaved(s.toString())){
|
||||
text = null
|
||||
hint = getString(R.string.hash_saved_hint)
|
||||
isEnabled = false
|
||||
} else {
|
||||
hint = null
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkboxRememberPath.isEnabled = true
|
||||
binding.editOldPassword.apply {
|
||||
hint = null
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
title = getString(R.string.change_password)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.textVolumeName.text = volume.name
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
|
||||
if (fingerprintProtector != null && volume.encryptedHash != null) {
|
||||
binding.textCurrentPasswordLabel.visibility = View.GONE
|
||||
binding.editCurrentPassword.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
editVolumePath.addTextChangedListener(textWatcher)
|
||||
editVolumeName.addTextChangedListener(textWatcher)
|
||||
binding.editNewPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
checkVolumePathThenChangePassword()
|
||||
if (!usfFingerprint || fingerprintProtector == null) {
|
||||
binding.checkboxSavePassword.visibility = View.GONE
|
||||
}
|
||||
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
changeVolumePassword()
|
||||
true
|
||||
}
|
||||
binding.buttonChangePassword.setOnClickListener {
|
||||
checkVolumePathThenChangePassword()
|
||||
}
|
||||
binding.button.setOnClickListener { changeVolumePassword() }
|
||||
}
|
||||
|
||||
fun checkVolumePathThenChangePassword() {
|
||||
loadVolumePath {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else if (!volumeFile.canWrite()){
|
||||
errorDirectoryNotWritable(R.string.change_pwd_cant_write_error_msg)
|
||||
} else {
|
||||
changePassword()
|
||||
}
|
||||
}
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun changePassword(givenHash: ByteArray? = null){
|
||||
val newPassword = binding.editNewPassword.text.toString().toCharArray()
|
||||
val newPasswordConfirm = binding.editNewPasswordConfirm.text.toString().toCharArray()
|
||||
private fun showCurrentPasswordInput() {
|
||||
binding.textCurrentPasswordLabel.visibility = View.VISIBLE
|
||||
binding.editCurrentPassword.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun changeVolumePassword() {
|
||||
val newPassword = CharArray(binding.editNewPassword.text.length)
|
||||
binding.editNewPassword.text.getChars(0, newPassword.size, newPassword, 0)
|
||||
val newPasswordConfirm = CharArray(binding.editPasswordConfirm.text.length)
|
||||
binding.editPasswordConfirm.text.getChars(0, newPasswordConfirm.size, newPasswordConfirm, 0)
|
||||
@SuppressLint("NewApi")
|
||||
if (!newPassword.contentEquals(newPasswordConfirm)) {
|
||||
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
} else {
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val oldPassword = binding.editOldPassword.text.toString().toCharArray()
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var changePasswordImmediately = true
|
||||
if (givenHash == null) {
|
||||
var volume: Volume? = null
|
||||
volumeDatabase.getVolumes().forEach { testVolume ->
|
||||
if (testVolume.name == currentVolumeName){
|
||||
volume = testVolume
|
||||
}
|
||||
}
|
||||
volume?.let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
it.hash?.let { hash ->
|
||||
it.iv?.let { iv ->
|
||||
currentVolumePath = if (it.isHidden){
|
||||
PathUtils.pathJoin(filesDir.path, it.name)
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
stopTask {
|
||||
loadPasswordHash(hash, iv, ::changePassword)
|
||||
}
|
||||
changePasswordImmediately = false
|
||||
}
|
||||
}
|
||||
var changeWithCurrentPassword = true
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
fingerprintProtector?.let {
|
||||
changeWithCurrentPassword = false
|
||||
it.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
showCurrentPasswordInput()
|
||||
volume.encryptedHash = null
|
||||
volume.iv = null
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
changeVolumePassword(newPassword, hash)
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
}
|
||||
}
|
||||
it.loadPasswordHash(volume.name, encryptedHash, iv)
|
||||
}
|
||||
if (changePasswordImmediately) {
|
||||
if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) {
|
||||
val volume = Volume(currentVolumeName, switchHiddenVolume.isChecked)
|
||||
if (volumeDatabase.isHashSaved(currentVolumeName)) {
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
if (checkboxRememberPath.isChecked) {
|
||||
volumeDatabase.saveVolume(volume)
|
||||
}
|
||||
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
stopTask {
|
||||
savePasswordHash(returnedHash) {
|
||||
onPasswordChanged()
|
||||
}
|
||||
}
|
||||
if (changeWithCurrentPassword) {
|
||||
changeVolumePassword(newPassword)
|
||||
}
|
||||
}
|
||||
Arrays.fill(newPasswordConfirm, 0.toChar())
|
||||
}
|
||||
|
||||
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (binding.checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var currentPassword: CharArray? = null
|
||||
if (givenHash == null) {
|
||||
currentPassword = CharArray(binding.editCurrentPassword.text.length)
|
||||
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
|
||||
}
|
||||
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) {
|
||||
if (volumeDatabase.isHashSaved(volume.name)) {
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
stopTask {
|
||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||
fingerprintProtector!!.let {
|
||||
it.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
// retry
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
finish()
|
||||
}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTask { onPasswordChanged() }
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.change_password_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
Arrays.fill(oldPassword, 0.toChar())
|
||||
}
|
||||
override fun doFinally(activity: AppCompatActivity) {
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
Arrays.fill(newPasswordConfirm, 0.toChar())
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.change_password_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
if (currentPassword != null)
|
||||
Arrays.fill(currentPassword, 0.toChar())
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
if (givenHash != null)
|
||||
Arrays.fill(givenHash, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPasswordChanged(){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.success_change_password)
|
||||
.setMessage(R.string.success_change_password_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Wiper.wipeEditText(binding.editOldPassword)
|
||||
Wiper.wipeEditText(binding.editNewPassword)
|
||||
Wiper.wipeEditText(binding.editNewPasswordConfirm)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ class ConstValues {
|
|||
const val MAX_KERNEL_WRITE = 128*1024
|
||||
const val wipe_passes = 2
|
||||
const val slideshow_delay: Long = 4000
|
||||
const val DEFAULT_THEME_VALUE = "dark_green"
|
||||
private val fileExtensions = mapOf(
|
||||
Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp")),
|
||||
Pair("video", listOf("mp4", "webm", "mkv", "mov")),
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.databinding.ActivityCreateBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class CreateActivity : VolumeActionActivity() {
|
||||
private var sessionID = -1
|
||||
private var isStartingExplorer = false
|
||||
private lateinit var binding: ActivityCreateBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCreateBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupLayout()
|
||||
setupFingerprintStuff(mayDecrypt = false)
|
||||
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
createVolume()
|
||||
true
|
||||
}
|
||||
binding.spinnerXchacha.adapter = ArrayAdapter(
|
||||
this@CreateActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
resources.getStringArray(R.array.encryption_cipher)
|
||||
).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
binding.spinnerXchacha.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (position == 1) {
|
||||
CustomAlertDialogBuilder(this@CreateActivity, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.xchacha_warning)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
binding.buttonCreate.setOnClickListener {
|
||||
createVolume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickSwitchHiddenVolume() {
|
||||
super.onClickSwitchHiddenVolume()
|
||||
if (switchHiddenVolume.isChecked){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.hidden_volume_warning)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun createVolume() {
|
||||
loadVolumePath {
|
||||
val password = binding.editPassword.text.toString().toCharArray()
|
||||
val passwordConfirm = binding.editPasswordConfirm.text.toString().toCharArray()
|
||||
if (!password.contentEquals(passwordConfirm)) {
|
||||
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
var goodDirectory = false
|
||||
if (!volumeFile.isDirectory) {
|
||||
if (volumeFile.mkdirs()) {
|
||||
goodDirectory = true
|
||||
} else {
|
||||
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
|
||||
}
|
||||
} else {
|
||||
val dirContent = volumeFile.list()
|
||||
if (dirContent != null) {
|
||||
if (dirContent.isEmpty()) {
|
||||
if (volumeFile.canWrite()) {
|
||||
goodDirectory = true
|
||||
} else {
|
||||
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
if (goodDirectory) {
|
||||
object: LoadingTask(this, themeValue, R.string.loading_msg_create) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
|
||||
0 -> 0
|
||||
1 -> 1
|
||||
else -> -1
|
||||
}
|
||||
if (GocryptfsVolume.createVolume(currentVolumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkboxSavePassword.isChecked){
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
|
||||
if (sessionID != -1) {
|
||||
if (checkboxRememberPath.isChecked) {
|
||||
if (volumeDatabase.isVolumeSaved(currentVolumeName)) { //cleaning old saved path
|
||||
volumeDatabase.removeVolume(Volume(currentVolumeName))
|
||||
}
|
||||
volumeDatabase.saveVolume(Volume(currentVolumeName, switchHiddenVolume.isChecked))
|
||||
}
|
||||
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
stopTask {
|
||||
savePasswordHash(returnedHash) {
|
||||
startExplorer()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTask { startExplorer() }
|
||||
}
|
||||
} else {
|
||||
stopTaskWithToast(R.string.open_volume_failed)
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.create_volume_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun doFinally(activity: AppCompatActivity) {
|
||||
Arrays.fill(password, 0.toChar())
|
||||
Arrays.fill(passwordConfirm, 0.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExplorer(){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.success_volume_create)
|
||||
.setMessage(R.string.success_volume_create_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val intent = Intent(this, ExplorerActivity::class.java)
|
||||
intent.putExtra("sessionID", sessionID)
|
||||
intent.putExtra("volume_name", File(currentVolumeName).name)
|
||||
startActivity(intent)
|
||||
isStartingExplorer = true
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
//Closing volume if leaving activity while showing dialog
|
||||
if (sessionID != -1 && !isStartingExplorer) {
|
||||
GocryptfsVolume(applicationContext, sessionID).close()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Wiper.wipeEditText(binding.editPassword)
|
||||
Wiper.wipeEditText(binding.editPasswordConfirm)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.UnrecoverableKeyException
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class FingerprintProtector private constructor(
|
||||
private val activity: FragmentActivity,
|
||||
private val themeValue: String,
|
||||
private val volumeDatabase: VolumeDatabase,
|
||||
) {
|
||||
|
||||
interface Listener {
|
||||
fun onHashStorageReset()
|
||||
fun onPasswordHashDecrypted(hash: ByteArray)
|
||||
fun onPasswordHashSaved()
|
||||
fun onFailed(pending: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val KEY_ALIAS = "Hash Key"
|
||||
private const val KEY_SIZE = 256
|
||||
private const val GCM_TAG_LEN = 128
|
||||
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
return if (!keyguardManager.isKeyguardSecure)
|
||||
1
|
||||
else when (BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> 0
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
fun new(
|
||||
activity: FragmentActivity,
|
||||
themeValue: String,
|
||||
volumeDatabase: VolumeDatabase,
|
||||
): FingerprintProtector? {
|
||||
return if (canAuthenticate(activity) == 0)
|
||||
FingerprintProtector(activity, themeValue, volumeDatabase)
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var listener: Listener
|
||||
private val biometricPrompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
if (
|
||||
errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
|
||||
errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON &&
|
||||
errorCode != BiometricPrompt.ERROR_TIMEOUT
|
||||
) {
|
||||
Toast.makeText(activity, activity.getString(R.string.biometric_error, errString), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
listener.onFailed(false)
|
||||
}
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
val cipherObject = result.cryptoObject?.cipher
|
||||
if (cipherObject != null) {
|
||||
try {
|
||||
when (cipherActionMode) {
|
||||
Cipher.ENCRYPT_MODE -> {
|
||||
val cipherText = cipherObject.doFinal(dataToProcess)
|
||||
volume.encryptedHash = cipherText
|
||||
volume.iv = cipherObject.iv
|
||||
if (volumeDatabase.addHash(volume))
|
||||
listener.onPasswordHashSaved()
|
||||
else
|
||||
listener.onFailed(false)
|
||||
}
|
||||
Cipher.DECRYPT_MODE -> {
|
||||
try {
|
||||
val plainText = cipherObject.doFinal(dataToProcess)
|
||||
listener.onPasswordHashDecrypted(plainText)
|
||||
} catch (e: AEADBadTagException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.MAC_verification_failed)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalBlockSizeException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.illegal_block_size_exception)
|
||||
.setMessage(R.string.illegal_block_size_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.error_cipher_null, Toast.LENGTH_SHORT).show()
|
||||
listener.onFailed(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
private lateinit var keyStore: KeyStore
|
||||
private lateinit var key: SecretKey
|
||||
private lateinit var cipher: Cipher
|
||||
private var isCipherReady = false
|
||||
private var cipherActionMode: Int? = null
|
||||
private lateinit var volume: Volume
|
||||
private lateinit var dataToProcess: ByteArray
|
||||
|
||||
private fun resetHashStorage() {
|
||||
try {
|
||||
keyStore.deleteEntry(KEY_ALIAS)
|
||||
} catch (e: KeyStoreException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
volumeDatabase.getVolumes().forEach { volume ->
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
isCipherReady = false
|
||||
Toast.makeText(activity, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
|
||||
listener.onHashStorageReset()
|
||||
}
|
||||
|
||||
private fun prepareCipher(): Boolean {
|
||||
if (!isCipherReady) {
|
||||
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
||||
keyStore.load(null)
|
||||
key = if (keyStore.containsAlias(KEY_ALIAS)) {
|
||||
try {
|
||||
keyStore.getKey(KEY_ALIAS, null) as SecretKey
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(activity.getString(R.string.unrecoverable_key_exception))
|
||||
.setMessage(activity.getString(R.string.unrecoverable_key_exception_msg, e.localizedMessage))
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
val builder = KeyGenParameterSpec.Builder(
|
||||
KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
)
|
||||
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
builder.setKeySize(KEY_SIZE)
|
||||
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
builder.setUserAuthenticationRequired(true)
|
||||
val keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
ANDROID_KEY_STORE
|
||||
)
|
||||
keyGenerator.init(builder.build())
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
cipher = Cipher.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
)
|
||||
isCipherReady = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun alertKeyPermanentlyInvalidatedException() {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.key_permanently_invalidated_exception)
|
||||
.setMessage(R.string.key_permanently_invalidated_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun savePasswordHash(volume: Volume, plainText: ByteArray) {
|
||||
this.volume = volume
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(R.string.encrypt_action_description))
|
||||
.setSubtitle(volume.shortName)
|
||||
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(activity.getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
cipherActionMode = Cipher.ENCRYPT_MODE
|
||||
if (prepareCipher()) {
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
dataToProcess = plainText
|
||||
biometricPrompt.authenticate(
|
||||
biometricPromptInfo,
|
||||
BiometricPrompt.CryptoObject(cipher)
|
||||
)
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPasswordHash(volumeName: String, cipherText: ByteArray, iv: ByteArray) {
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(R.string.decrypt_action_description))
|
||||
.setSubtitle(volumeName)
|
||||
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(activity.getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
cipherActionMode = Cipher.DECRYPT_MODE
|
||||
if (prepareCipher()) {
|
||||
dataToProcess = cipherText
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
||||
biometricPrompt.authenticate(
|
||||
biometricPromptInfo,
|
||||
BiometricPrompt.CryptoObject(cipher)
|
||||
)
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
|
|||
const val KeyLen = 32
|
||||
const val ScryptDefaultLogN = 16
|
||||
const val DefaultBS = 4096
|
||||
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String): Boolean
|
||||
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean
|
||||
external fun init(root_cipher_dir: String, password: CharArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
|
||||
external fun changePassword(root_cipher_dir: String, old_password: CharArray?, givenHash: ByteArray?, new_password: CharArray, returnedHash: ByteArray?): Boolean
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
|
@ -19,7 +18,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
|||
startTask()
|
||||
}
|
||||
abstract fun doTask(activity: AppCompatActivity)
|
||||
open fun doFinally(activity: AppCompatActivity){}
|
||||
private fun startTask() {
|
||||
dialogLoading.show()
|
||||
Thread {
|
||||
|
@ -27,7 +25,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
|||
if (!isStopped){
|
||||
dialogLoading.dismiss()
|
||||
}
|
||||
activity.runOnUiThread { doFinally(activity) }
|
||||
}.start()
|
||||
}
|
||||
fun stopTask(onUiThread: (() -> Unit)?){
|
||||
|
@ -39,7 +36,4 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
|||
}
|
||||
}
|
||||
}
|
||||
protected fun stopTaskWithToast(stringId: Int){
|
||||
stopTask { Toast.makeText(activity, stringId, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
}
|
|
@ -1,19 +1,69 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
|
||||
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
|
||||
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private lateinit var volumeAdapter: VolumeAdapter
|
||||
private var fingerprintProtector: FingerprintProtector? = null
|
||||
private var usfFingerprint: Boolean = false
|
||||
private var usfKeepOpen: Boolean = false
|
||||
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
when (result.resultCode) {
|
||||
AddVolumeActivity.RESULT_VOLUME_ADDED -> {
|
||||
volumeAdapter.apply {
|
||||
volumes = volumeDatabase.getVolumes()
|
||||
notifyItemInserted(volumes.size)
|
||||
}
|
||||
binding.textNoVolumes.visibility = View.GONE
|
||||
}
|
||||
AddVolumeActivity.RESULT_HASH_STORAGE_RESET -> {
|
||||
volumeAdapter.refresh()
|
||||
binding.textNoVolumes.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
private var changePasswordPosition: Int? = null
|
||||
private var changePassword = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
changePasswordPosition?.let {
|
||||
volumeAdapter.selectedItems.remove(it)
|
||||
volumeAdapter.onVolumeChanged(it)
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
private var pickMode = false
|
||||
private var dropMode = false
|
||||
private var shouldCloseVolume = true // used when launched to pick file from another volume
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
if (sharedPrefs.getBoolean("applicationFirstOpening", true)) {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
|
@ -28,20 +78,141 @@ class MainActivity : BaseActivity() {
|
|||
.setOnDismissListener { sharedPrefs.edit().putBoolean("applicationFirstOpening", false).apply() }
|
||||
.show()
|
||||
}
|
||||
binding.buttonOpen.setOnClickListener {
|
||||
startActivity(OpenActivity::class.java)
|
||||
pickMode = intent.action == "pick"
|
||||
dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
volumeAdapter = VolumeAdapter(
|
||||
this,
|
||||
volumeDatabase,
|
||||
!pickMode && !dropMode,
|
||||
!dropMode,
|
||||
::onVolumeItemClick,
|
||||
::onVolumeItemLongClick,
|
||||
)
|
||||
binding.recyclerViewVolumes.adapter = volumeAdapter
|
||||
binding.recyclerViewVolumes.layoutManager = LinearLayoutManager(this)
|
||||
if (volumeAdapter.volumes.isEmpty()) {
|
||||
binding.textNoVolumes.visibility = View.VISIBLE
|
||||
}
|
||||
binding.buttonCreate.setOnClickListener {
|
||||
startActivity(CreateActivity::class.java)
|
||||
if (pickMode) {
|
||||
title = getString(R.string.select_volume)
|
||||
binding.fab.visibility = View.GONE
|
||||
} else {
|
||||
binding.fab.setOnClickListener {
|
||||
addVolume.launch(Intent(this, AddVolumeActivity::class.java))
|
||||
}
|
||||
}
|
||||
binding.buttonChangePassword.setOnClickListener {
|
||||
startActivity(ChangePasswordActivity::class.java)
|
||||
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVolumeItemClick(volume: Volume, position: Int) {
|
||||
if (volumeAdapter.selectedItems.isEmpty())
|
||||
openVolume(volume, position)
|
||||
else
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun onVolumeItemLongClick() {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun unselectAll() {
|
||||
volumeAdapter.unSelectAll()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun removeVolumes(volumes: List<Volume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
|
||||
if (i < volumes.size) {
|
||||
if (volumes[i].isHidden) {
|
||||
if (doDeleteVolumeContent == null) {
|
||||
val dialogBinding = DialogDeleteVolumeBinding.inflate(layoutInflater)
|
||||
dialogBinding.textContent.text = getString(R.string.delete_hidden_volume_question, volumes[i].name)
|
||||
// show checkbox only if there is at least one other hidden volume
|
||||
for (j in (i+1 until volumes.size)) {
|
||||
if (volumes[j].isHidden) {
|
||||
dialogBinding.checkboxApplyToAll.visibility = View.VISIBLE
|
||||
break
|
||||
}
|
||||
}
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(R.string.forget_only) { _, _ ->
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) false else null)
|
||||
}
|
||||
.setNegativeButton(R.string.delete_volume) { _, _ ->
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
volumeAdapter.refresh()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
if (doDeleteVolumeContent) {
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
}
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
volumeAdapter.refresh()
|
||||
invalidateOptionsMenu()
|
||||
if (volumeAdapter.volumes.isEmpty()) {
|
||||
binding.textNoVolumes.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_settings -> {
|
||||
android.R.id.home -> {
|
||||
if (pickMode || dropMode) {
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
finish()
|
||||
} else {
|
||||
unselectAll()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
volumeAdapter.selectAll()
|
||||
invalidateOptionsMenu()
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
|
||||
removeVolumes(selectedVolumes)
|
||||
true
|
||||
}
|
||||
R.id.forget_password -> {
|
||||
for (i in volumeAdapter.selectedItems) {
|
||||
if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
|
||||
volumeAdapter.onVolumeChanged(i)
|
||||
}
|
||||
unselectAll()
|
||||
true
|
||||
}
|
||||
R.id.change_password -> {
|
||||
changePasswordPosition = volumeAdapter.selectedItems.elementAt(0)
|
||||
changePassword.launch(Intent(this, ChangePasswordActivity::class.java).apply {
|
||||
putExtra("volume", volumeAdapter.volumes[changePasswordPosition!!])
|
||||
})
|
||||
true
|
||||
}
|
||||
R.id.settings -> {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
true
|
||||
|
@ -52,11 +223,199 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_activity, menu)
|
||||
menu.findItem(R.id.settings).isVisible = !pickMode && !dropMode
|
||||
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
|
||||
menu.findItem(R.id.select_all).isVisible = isSelecting && !pickMode && !dropMode
|
||||
menu.findItem(R.id.remove).isVisible = isSelecting && !pickMode && !dropMode
|
||||
var showForgetPassword = isSelecting
|
||||
if (isSelecting) {
|
||||
for (volume in volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }) {
|
||||
if (volume.encryptedHash == null) {
|
||||
showForgetPassword = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
menu.findItem(R.id.forget_password).isVisible = showForgetPassword && !pickMode
|
||||
menu.findItem(R.id.change_password).isVisible =
|
||||
!pickMode && !dropMode &&
|
||||
volumeAdapter.selectedItems.size == 1 &&
|
||||
volumeAdapter.volumes[volumeAdapter.selectedItems.elementAt(0)].canWrite(filesDir.path)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || pickMode || dropMode)
|
||||
return true
|
||||
}
|
||||
|
||||
fun <T> startActivity(clazz: Class<T>) {
|
||||
val intent = Intent(this, clazz)
|
||||
startActivity(intent)
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
private fun openVolume(volume: Volume, position: Int) {
|
||||
var askForPassword = true
|
||||
fingerprintProtector?.let { fingerprintProtector ->
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
askForPassword = false
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
|
||||
Arrays.fill(hash, 0)
|
||||
if (sessionId != -1)
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {}
|
||||
}
|
||||
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (askForPassword)
|
||||
askForPassword(volume, position)
|
||||
}
|
||||
|
||||
private fun onPasswordSubmitted(volume: Volume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
|
||||
val password = CharArray(dialogBinding.editPassword.text.length)
|
||||
dialogBinding.editPassword.text.getChars(0, password.size, password, 0)
|
||||
// openVolumeWithPassword is responsible for wiping the password
|
||||
openVolumeWithPassword(
|
||||
volume,
|
||||
position,
|
||||
password,
|
||||
dialogBinding.checkboxSavePassword.isChecked,
|
||||
)
|
||||
}
|
||||
|
||||
private fun askForPassword(volume: Volume, position: Int) {
|
||||
val dialogBinding = DialogOpenVolumeBinding.inflate(layoutInflater)
|
||||
if (!usfFingerprint || fingerprintProtector == null) {
|
||||
dialogBinding.checkboxSavePassword.visibility = View.GONE
|
||||
}
|
||||
val dialog = CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.open_dialog_title)
|
||||
.setView(dialogBinding.root)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.open) { _, _ ->
|
||||
onPasswordSubmitted(volume, position, dialogBinding)
|
||||
}
|
||||
.create()
|
||||
dialogBinding.editPassword.setOnEditorActionListener { _, _, _ ->
|
||||
dialog.dismiss()
|
||||
onPasswordSubmitted(volume, position, dialogBinding)
|
||||
true
|
||||
}
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
|
||||
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (savePasswordHash && usfFingerprint) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
|
||||
Arrays.fill(password, 0.toChar())
|
||||
if (sessionId != -1) {
|
||||
val fingerprintProtector = fingerprintProtector
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
if (savePasswordHash && returnedHash != null && fingerprintProtector != null)
|
||||
stopTask {
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
volumeAdapter.onVolumeChanged(position)
|
||||
startExplorer(sessionId, volume.shortName)
|
||||
}
|
||||
private var isClosed = false
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!isClosed) {
|
||||
GocryptfsVolume(this@MainActivity, sessionId).close()
|
||||
isClosed = true
|
||||
}
|
||||
Arrays.fill(returnedHash, 0)
|
||||
}
|
||||
}
|
||||
fingerprintProtector.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
else
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
} else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_volume_failed_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener {
|
||||
askForPassword(volume, position)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExplorer(sessionId: Int, volumeShortName: String) {
|
||||
var explorerIntent: Intent? = null
|
||||
if (dropMode) { //import via android share menu
|
||||
explorerIntent = Intent(this, ExplorerActivityDrop::class.java)
|
||||
explorerIntent.action = intent.action //forward action
|
||||
explorerIntent.putExtras(intent.extras!!) //forward extras
|
||||
} else if (pickMode) {
|
||||
explorerIntent = Intent(this, ExplorerActivityPick::class.java)
|
||||
explorerIntent.putExtra("originalSessionID", intent.getIntExtra("sessionID", -1))
|
||||
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
|
||||
}
|
||||
if (explorerIntent == null) {
|
||||
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
|
||||
}
|
||||
explorerIntent.putExtra("sessionID", sessionId)
|
||||
explorerIntent.putExtra("volume_name", volumeShortName)
|
||||
startActivity(explorerIntent)
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
if (dropMode || pickMode)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (volumeAdapter.selectedItems.isNotEmpty()) {
|
||||
unselectAll()
|
||||
} else {
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (pickMode && !usfKeepOpen) {
|
||||
finish()
|
||||
if (shouldCloseVolume) {
|
||||
val sessionID = intent.getIntExtra("sessionID", -1)
|
||||
if (sessionID != -1) {
|
||||
GocryptfsVolume(this, sessionID).close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,265 +0,0 @@
|
|||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.MenuItem
|
||||