Multi volume openings

This commit is contained in:
Matéo Duparc 2023-03-07 23:25:17 +01:00
parent 2d165c4a20
commit 1a1d3ea570
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
38 changed files with 436 additions and 393 deletions

View File

@ -88,9 +88,11 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.appcompat:appcompat:1.6.0"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
def lifecycle_version = "2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.sqlite:sqlite-ktx:2.3.0"
implementation "androidx.preference:preference-ktx:1.2.0"

View File

@ -30,7 +30,8 @@
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/BaseTheme">
android:theme="@style/BaseTheme"
android:name=".VolumeManagerApp">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -4,7 +4,6 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
open class BaseActivity: AppCompatActivity() {
protected lateinit var sharedPrefs: SharedPreferences
@ -13,7 +12,7 @@ open class BaseActivity: AppCompatActivity() {
private var shouldCheckTheme = true
override fun onCreate(savedInstanceState: Bundle?) {
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
sharedPrefs = (application as VolumeManagerApp).sharedPreferences
themeValue = sharedPrefs.getString(Constants.THEME_VALUE_KEY, Constants.DEFAULT_THEME_VALUE)!!
if (shouldCheckTheme && applyCustomTheme) {
when (themeValue) {

View File

@ -17,7 +17,6 @@ import android.view.animation.RotateAnimation
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.activity.addCallback
import androidx.annotation.RequiresApi
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.*
@ -29,7 +28,6 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
@ -63,14 +61,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding.imageTimer.setImageResource(R.drawable.icon_timer_off)
}
}
private var usf_keep_open = false
private lateinit var sensorOrientationListener: SensorOrientationListener
private var previousOrientation: Float = 0f
private lateinit var orientedIcons: List<ImageView>
private lateinit var encryptedVolume: EncryptedVolume
private lateinit var outputDirectory: String
private var isFinishingIntentionally = false
private var isAskingPermissions = false
private var permissionsGranted = false
private lateinit var executor: Executor
private lateinit var cameraProvider: ProcessCameraProvider
@ -92,7 +87,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
@ -103,7 +97,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
permissionsGranted = true
} else {
isAskingPermissions = true
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
}
} else {
@ -220,7 +213,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding.takePhotoButton.visibility = View.GONE
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
isAskingPermissions = true
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), AUDIO_PERMISSION_REQUEST_CODE)
}
}
@ -280,17 +272,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
else -> false
}
}
onBackPressedDispatcher.addCallback(this) {
isFinishingIntentionally = true
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
isAskingPermissions = false
if (grantResults.size == 1) {
when (requestCode) {
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@ -301,10 +287,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
.setTitle(R.string.error)
.setMessage(R.string.camera_perm_needed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
isFinishingIntentionally = true
finish()
}.show()
.setPositiveButton(R.string.ok) { _, _ -> finish() }.show()
}
AUDIO_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (videoCapture != null) {
@ -431,10 +414,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
.setTitle(R.string.error)
.setMessage(R.string.picture_save_failed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
isFinishingIntentionally = true
finish()
}
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
}
}
@ -485,32 +465,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
}
}
override fun onDestroy() {
super.onDestroy()
if (!isFinishingIntentionally) {
encryptedVolume.close()
RestrictedFileProvider.wipeAll(this)
}
}
override fun onStop() {
super.onStop()
if (!isFinishing && !usf_keep_open){
finish()
}
}
override fun onPause() {
super.onPause()
sensorOrientationListener.remove(this)
if (!isAskingPermissions && !usf_keep_open) {
finish()
}
}
override fun onResume() {
super.onResume()
sensorOrientationListener.addListener(this)
if (encryptedVolume.isClosed()) {
finish()
} else {
sensorOrientationListener.addListener(this)
}
}
override fun onOrientationChange(newOrientation: Int) {

View File

@ -20,12 +20,10 @@ import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
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.explorers.ExplorerRouter
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -33,11 +31,15 @@ import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
companion object {
private const val OPEN_DEFAULT_VOLUME = "openDefault"
}
private lateinit var binding: ActivityMainBinding
private lateinit var volumeDatabase: VolumeDatabase
private lateinit var volumeManager: VolumeManager
private lateinit var volumeAdapter: VolumeAdapter
private lateinit var volumeOpener: VolumeOpener
private var usfKeepOpen: Boolean = false
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if ((explorerRouter.pickMode || explorerRouter.dropMode) && result.resultCode != AddVolumeActivity.RESULT_USER_BACK) {
setResult(result.resultCode, result.data) // forward result
@ -54,7 +56,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
private lateinit var fileOperationService: FileOperationService
private lateinit var explorerRouter: ExplorerRouter
private var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -80,10 +81,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
.show()
}
explorerRouter = ExplorerRouter(this, intent)
volumeManager = (application as VolumeManagerApp).volumeManager
volumeDatabase = VolumeDatabase(this)
volumeAdapter = VolumeAdapter(
this,
volumeDatabase,
(application as VolumeManagerApp).volumeManager,
!explorerRouter.pickMode && !explorerRouter.dropMode,
!explorerRouter.dropMode,
this,
@ -100,30 +103,31 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
addVolume.launch(Intent(this, AddVolumeActivity::class.java).also {
if (explorerRouter.dropMode || explorerRouter.pickMode) {
IntentUtils.forwardIntent(intent, it)
shouldCloseVolume = false
}
})
}
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
volumeOpener = VolumeOpener(this)
volumeOpener.defaultVolumeName?.let { name ->
try {
openVolume(volumeAdapter.volumes.first { it.name == name })
} catch (e: NoSuchElementException) {
unsetDefaultVolume()
}
}
onBackPressedDispatcher.addCallback(this) {
if (volumeAdapter.selectedItems.isNotEmpty()) {
unselectAll()
} else {
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
volumeOpener.defaultVolumeName?.let { name ->
val state = savedInstanceState?.getBoolean(OPEN_DEFAULT_VOLUME)
if (state == true || state == null) {
val volumeData = volumeAdapter.volumes.first { it.name == name }
if (!volumeManager.isOpen(volumeData)) {
try {
openVolume(volumeData)
} catch (e: NoSuchElementException) {
unsetDefaultVolume()
}
}
}
}
Intent(this, FileOperationService::class.java).also {
bindService(it, object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
@ -148,6 +152,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
volumeOpener.defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(OPEN_DEFAULT_VOLUME, false)
}
override fun onSelectionChanged(size: Int) {
title = if (size == 0) {
getString(R.string.app_name)
@ -179,6 +188,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu()
}
private fun removeVolume(volume: VolumeData) {
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
volumeDatabase.removeVolume(volume.name)
}
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
if (i < volumes.size) {
if (volumes[i].isHidden) {
@ -196,12 +210,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
.setTitle(R.string.warning)
.setView(dialogBinding.root)
.setPositiveButton(R.string.forget_only) { _, _ ->
volumeDatabase.removeVolume(volumes[i].name)
removeVolume(volumes[i])
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)
removeVolume(volumes[i])
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
}
.setOnCancelListener {
@ -213,11 +227,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (doDeleteVolumeContent) {
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
}
volumeDatabase.removeVolume(volumes[i].name)
removeVolume(volumes[i])
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
volumeDatabase.removeVolume(volumes[i].name)
removeVolume(volumes[i])
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
@ -241,9 +255,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
return when (item.itemId) {
android.R.id.home -> {
if (explorerRouter.pickMode || explorerRouter.dropMode) {
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
finish()
} else {
unselectAll()
@ -255,6 +266,15 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu()
true
}
R.id.lock -> {
volumeAdapter.selectedItems.forEach {
volumeManager.getVolumeId(volumeAdapter.volumes[it])?.let { id ->
volumeManager.closeVolume(id)
}
}
unselectAll()
true
}
R.id.remove -> {
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
removeVolumes(selectedVolumes)
@ -325,6 +345,9 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = isSelecting
menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any {
i -> volumeManager.isOpen(volumeAdapter.volumes[i])
}
menu.findItem(R.id.remove).isVisible = isSelecting
menu.findItem(R.id.delete_password_hash).isVisible =
isSelecting &&
@ -456,11 +479,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
volumeAdapter.refresh()
}
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
override fun onVolumeOpened(id: Int) {
startActivity(explorerRouter.getExplorerIntent(id, volume.shortName))
if (explorerRouter.dropMode || explorerRouter.pickMode) {
finish()
}
@ -468,18 +488,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
})
}
override fun onResume() {
super.onResume()
shouldCloseVolume = true
}
override fun onStop() {
super.onStop()
volumeOpener.wipeSensitive()
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
finish()
}
}
}

View File

@ -44,6 +44,17 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
}
}
override fun equals(other: Any?): Boolean {
if (other !is VolumeData) {
return false
}
return other.name == name && other.isHidden == isHidden
}
override fun hashCode(): Int {
return name.hashCode()+isHidden.hashCode()
}
companion object {
const val VOLUMES_DIRECTORY = "volumes"

View File

@ -0,0 +1,42 @@
package sushi.hardcore.droidfs
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
class VolumeManager {
private var id = 0
private val volumes = HashMap<Int, EncryptedVolume>()
private val volumesData = HashMap<VolumeData, Int>()
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume
volumesData[data] = id
return id++
}
fun isOpen(volume: VolumeData): Boolean {
return volumesData.containsKey(volume)
}
fun getVolumeId(volume: VolumeData): Int? {
return volumesData[volume]
}
fun getVolume(id: Int): EncryptedVolume? {
return volumes[id]
}
fun closeVolume(id: Int) {
volumes.remove(id)?.let { volume ->
volume.close()
volumesData.filter { it.value == id }.forEach {
volumesData.remove(it.key)
}
}
}
fun closeAll() {
volumes.forEach { it.value.close() }
volumes.clear()
volumesData.clear()
}
}

View File

@ -0,0 +1,49 @@
package sushi.hardcore.droidfs
import android.app.Application
import android.content.SharedPreferences
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object {
private const val USF_KEEP_OPEN_KEY = "usf_keep_open"
}
lateinit var sharedPreferences: SharedPreferences
private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == USF_KEEP_OPEN_KEY) {
reloadUsfKeepOpen()
}
}
private var usfKeepOpen = false
var isStartingExternalApp = false
val volumeManager = VolumeManager()
override fun onCreate() {
super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this).apply {
registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
}
reloadUsfKeepOpen()
}
private fun reloadUsfKeepOpen() {
usfKeepOpen = sharedPreferences.getBoolean(USF_KEEP_OPEN_KEY, false)
}
override fun onResume(owner: LifecycleOwner) {
isStartingExternalApp = false
}
override fun onStop(owner: LifecycleOwner) {
if (!isStartingExternalApp && !usfKeepOpen) {
volumeManager.closeAll()
RestrictedFileProvider.wipeAll(applicationContext)
}
}
}

View File

@ -22,7 +22,7 @@ class VolumeOpener(
) {
interface VolumeOpenerCallbacks {
fun onHashStorageReset() {}
fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String)
fun onVolumeOpened(id: Int)
}
private val volumeDatabase = VolumeDatabase(activity)
@ -31,6 +31,7 @@ class VolumeOpener(
var themeValue = sharedPrefs.getString(Constants.THEME_VALUE_KEY, Constants.DEFAULT_THEME_VALUE)!!
var defaultVolumeName: String? = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
private var dialogBinding: DialogOpenVolumeBinding? = null
private val volumeManager = (activity.application as VolumeManagerApp).volumeManager
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -40,54 +41,59 @@ class VolumeOpener(
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
fun openVolume(volume: VolumeData, isVolumeSaved: Boolean, callbacks: VolumeOpenerCallbacks) {
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
Toast.makeText(activity, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
return
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
Toast.makeText(activity, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show()
return
}
var askForPassword = true
fingerprintProtector?.let { fingerprintProtector ->
volume.encryptedHash?.let { encryptedHash ->
volume.iv?.let { iv ->
askForPassword = false
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
callbacks.onHashStorageReset()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return encryptedVolume
val volumeId = volumeManager.getVolumeId(volume)
if (volumeId == null) {
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
Toast.makeText(activity, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
return
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
Toast.makeText(activity, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show()
return
}
var askForPassword = true
fingerprintProtector?.let { fingerprintProtector ->
volume.encryptedHash?.let { encryptedHash ->
volume.iv?.let { iv ->
askForPassword = false
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
callbacks.onHashStorageReset()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return encryptedVolume
}
}.startTask(activity.lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
}
}
}.startTask(activity.lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {
if (!pending) {
askForPassword(volume, isVolumeSaved, callbacks)
}
}
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {
if (!pending) {
askForPassword(volume, isVolumeSaved, callbacks)
}
}
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
}
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
}
}
}
if (askForPassword) {
askForPassword(volume, isVolumeSaved, callbacks)
if (askForPassword) {
askForPassword(volume, isVolumeSaved, callbacks)
}
} else {
callbacks.onVolumeOpened(volumeId)
}
}
@ -188,7 +194,7 @@ class VolumeOpener(
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0)
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
}
private var isClosed = false
override fun onFailed(pending: Boolean) {
@ -201,7 +207,7 @@ class VolumeOpener(
}
fingerprintProtector.savePasswordHash(volume, returnedHash.value!!)
} else {
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
}
}
}

View File

@ -8,15 +8,18 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
class VolumeAdapter(
private val context: Context,
private val volumeDatabase: VolumeDatabase,
private val volumeManager: VolumeManager,
private val allowSelection: Boolean,
private val showReadOnly: Boolean,
private val listener: Listener,
@ -80,16 +83,12 @@ class VolumeAdapter(
fun bind(position: Int) {
val volume = volumes[position]
itemView.findViewById<TextView>(R.id.text_volume_name).text = volume.shortName
itemView.findViewById<ImageView>(R.id.image_icon).setImageResource(R.drawable.icon_volume)
itemView.findViewById<TextView>(R.id.text_path).text = if (volume.isHidden)
context.getString(R.string.hidden_volume)
else
volume.name
itemView.findViewById<ImageView>(R.id.icon_fingerprint).visibility = if (volume.encryptedHash == null) {
View.GONE
} else {
View.VISIBLE
}
itemView.findViewById<ImageView>(R.id.icon_unlocked).isVisible = volumeManager.isOpen(volume)
itemView.findViewById<ImageView>(R.id.icon_fingerprint).isVisible = volume.encryptedHash != null
itemView.findViewById<TextView>(R.id.text_info).text = context.getString(
if (volume.canWrite(context.filesDir.path)) {
R.string.volume_type

View File

@ -4,11 +4,8 @@ import android.os.Bundle
import android.view.MenuItem
import androidx.activity.addCallback
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityAddVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
class AddVolumeActivity: BaseActivity() {
@ -19,15 +16,12 @@ class AddVolumeActivity: BaseActivity() {
private lateinit var binding: ActivityAddVolumeBinding
private lateinit var explorerRouter: ExplorerRouter
private lateinit var volumeOpener: VolumeOpener
private var usfKeepOpen = false
var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddVolumeBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
explorerRouter = ExplorerRouter(this, intent)
volumeOpener = VolumeOpener(this)
if (savedInstanceState == null) {
@ -41,7 +35,6 @@ class AddVolumeActivity: BaseActivity() {
}
onBackPressedDispatcher.addCallback(this) {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
@ -53,7 +46,6 @@ class AddVolumeActivity: BaseActivity() {
supportFragmentManager.popBackStack()
else {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
finish()
}
}
@ -70,21 +62,19 @@ class AddVolumeActivity: BaseActivity() {
)
}
fun startExplorer(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
shouldCloseVolume = false
fun startExplorer(volumeId: Int, volumeShortName: String) {
startActivity(explorerRouter.getExplorerIntent(volumeId, volumeShortName))
finish()
}
fun onVolumeSelected(volume: VolumeData, rememberVolume: Boolean) {
if (rememberVolume) {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
finish()
} else {
volumeOpener.openVolume(volume, false, object : VolumeOpener.VolumeOpenerCallbacks {
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startExplorer(encryptedVolume, volumeShortName)
override fun onVolumeOpened(id: Int) {
startExplorer(id, volume.shortName)
}
})
}
@ -106,18 +96,4 @@ class AddVolumeActivity: BaseActivity() {
.addToBackStack(null)
.commit()
}
override fun onStart() {
super.onStart()
shouldCloseVolume = true
}
override fun onStop() {
super.onStop()
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
finish()
}
}
}

View File

@ -158,11 +158,7 @@ class CreateVolumeFragment: Fragment() {
} else {
null
}
val encryptedVolume = if (rememberVolume) {
null
} else {
ObjRef<EncryptedVolume?>(null)
}
val encryptedVolume = ObjRef<EncryptedVolume?>(null)
object: LoadingTask<Byte>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
private fun generateResult(success: Boolean, volumeType: Byte): Byte {
return if (success) {
@ -223,6 +219,9 @@ class CreateVolumeFragment: Fragment() {
isVolumeSaved = saveVolume(volume)
}
}
val volumeId = encryptedVolume.value?.let {
(activity?.application as VolumeManagerApp).volumeManager.insert(it, volume)
}
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (isVolumeSaved && binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
@ -235,31 +234,31 @@ class CreateVolumeFragment: Fragment() {
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated(encryptedVolume?.value, volume.shortName)
onVolumeCreated(volumeId, volume.shortName)
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated(encryptedVolume?.value, volume.shortName)
onVolumeCreated(volumeId, volume.shortName)
}
}
}
it.savePasswordHash(volume, returnedHash.value!!)
}
} else {
onVolumeCreated(encryptedVolume?.value, volume.shortName)
onVolumeCreated(volumeId, volume.shortName)
}
}
}
}
}
private fun onVolumeCreated(encryptedVolume: EncryptedVolume?, volumeShortName: String) {
private fun onVolumeCreated(id: Int?, volumeShortName: String) {
(activity as AddVolumeActivity).apply {
if (rememberVolume || encryptedVolume == null) {
if (rememberVolume || id == null) {
finish()
} else {
startExplorer(encryptedVolume, volumeShortName)
startExplorer(id, volumeShortName)
}
}
}

View File

@ -24,6 +24,7 @@ import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding
import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -62,6 +63,7 @@ class SelectPathFragment: Fragment() {
if (uri != null)
onDirectoryPicked(uri)
}
private lateinit var app: VolumeManagerApp
private var themeValue = Constants.DEFAULT_THEME_VALUE
private lateinit var volumeDatabase: VolumeDatabase
private lateinit var filesDir: String
@ -81,6 +83,7 @@ class SelectPathFragment: Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
app = requireActivity().application as VolumeManagerApp
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
originalRememberVolume = sharedPrefs.getBoolean(Constants.REMEMBER_VOLUME_KEY, true)
binding.switchRemember.isChecked = originalRememberVolume
@ -105,6 +108,7 @@ class SelectPathFragment: Fragment() {
if (Environment.isExternalStorageManager()) {
launchPickDirectory()
} else {
app.isStartingExternalApp = true
startActivity(Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:"+requireContext().packageName)))
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -119,6 +123,7 @@ class SelectPathFragment: Fragment() {
) {
launchPickDirectory()
} else {
app.isStartingExternalApp = true
askStoragePermissions.launch(
arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
@ -149,7 +154,7 @@ class SelectPathFragment: Fragment() {
}
private fun launchPickDirectory() {
(activity as AddVolumeActivity).shouldCloseVolume = false
app.isStartingExternalApp = true
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
}

View File

@ -15,7 +15,7 @@ import sushi.hardcore.droidfs.util.SQLUtil.appendSelectionArgs
import sushi.hardcore.droidfs.util.SQLUtil.concatenateWhere
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.util.*
import java.util.UUID
import java.util.regex.Pattern
class RestrictedFileProvider: ContentProvider() {

View File

@ -13,6 +13,7 @@ import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@ -22,20 +23,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.*
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_viewers.*
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -46,6 +42,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private var foldersFirst = true
private var mapFolders = true
private var currentSortOrderIndex = 0
private var volumeId = -1
protected lateinit var encryptedVolume: EncryptedVolume
private lateinit var volumeName: String
private lateinit var explorerViewModel: ExplorerViewModel
@ -58,10 +55,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
protected val taskScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
protected lateinit var explorerElements: MutableList<ExplorerElement>
protected lateinit var explorerAdapter: ExplorerElementAdapter
private var isCreating = true
protected var isStartingActivity = false
protected lateinit var app: VolumeManagerApp
private var usf_open = false
protected var usf_keep_open = false
private lateinit var linearLayoutManager: LinearLayoutManager
private var isUsingListLayout = true
private lateinit var layoutIcon: ImageButton
@ -76,10 +71,11 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
app = application as VolumeManagerApp
usf_open = sharedPrefs.getBoolean("usf_open", false)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
volumeName = intent.getStringExtra("volume_name") ?: ""
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
volumeName = intent.getStringExtra("volumeName") ?: ""
volumeId = intent.getIntExtra("volumeId", -1)
encryptedVolume = app.volumeManager.getVolume(volumeId)!!
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -118,6 +114,19 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
isUsingListLayout = sharedPrefs.getBoolean("useListLayout", true)
layoutIcon = findViewById(R.id.layout_icon)
setRecyclerViewLayout()
onBackPressedDispatcher.addCallback(this) {
if (explorerAdapter.selectedItems.isEmpty()) {
val parentPath = PathUtils.getParentPath(currentDirectoryPath)
if (parentPath == currentDirectoryPath) {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
} else {
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
}
} else {
unselectAll()
}
}
layoutIcon.setOnClickListener {
isUsingListLayout = !isUsingListLayout
setRecyclerViewLayout()
@ -171,18 +180,17 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
}
private fun startFileViewer(cls: Class<*>, filePath: String){
private fun startFileViewer(cls: Class<*>, filePath: String) {
val intent = Intent(this, cls).apply {
putExtra("path", filePath)
putExtra("volume", encryptedVolume)
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
}
isStartingActivity = true
startActivity(intent)
}
private fun openWithExternalApp(fullPath: String){
isStartingActivity = true
private fun openWithExternalApp(fullPath: String) {
app.isStartingExternalApp = true
ExternalProvider.open(this, themeValue, encryptedVolume, fullPath)
}
@ -332,28 +340,18 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
}
private fun askCloseVolume() {
private fun askLockVolume() {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(R.string.ask_close_volume)
.setPositiveButton(R.string.ok) { _, _ -> closeVolumeOnUserExit() }
.setMessage(R.string.ask_lock_volume)
.setPositiveButton(R.string.ok) { _, _ ->
app.volumeManager.closeVolume(volumeId)
finish()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun onBackPressed() {
if (explorerAdapter.selectedItems.isEmpty()) {
val parentPath = PathUtils.getParentPath(currentDirectoryPath)
if (parentPath == currentDirectoryPath) {
askCloseVolume()
} else {
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
}
} else {
unselectAll()
}
}
private fun createFolder(folderName: String){
if (folderName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
@ -508,9 +506,9 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
val noItemSelected = explorerAdapter.selectedItems.isEmpty()
val iconColor = ContextCompat.getColor(this, R.color.neutralIconTint)
setMenuIconTint(menu, iconColor, R.id.sort, R.drawable.icon_sort)
setMenuIconTint(menu, iconColor, R.id.decrypt, R.drawable.icon_decrypt)
setMenuIconTint(menu, iconColor, R.id.share, R.drawable.icon_share)
menu.findItem(R.id.sort).isVisible = noItemSelected
menu.findItem(R.id.lock).isVisible = noItemSelected
menu.findItem(R.id.close).isVisible = noItemSelected
supportActionBar?.setDisplayHomeAsUpEnabled(!noItemSelected)
if (!noItemSelected) {
@ -577,55 +575,33 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
true
}
R.id.close -> {
askCloseVolume()
finish()
true
}
R.id.lock -> {
askLockVolume()
true
}
else -> super.onOptionsItemSelected(item)
}
}
protected open fun closeVolumeOnUserExit() {
finish()
}
protected open fun closeVolumeOnDestroy() {
taskScope.cancel()
if (!encryptedVolume.isClosed()) {
encryptedVolume.close()
}
RestrictedFileProvider.wipeAll(this) //additional security
}
override fun onDestroy() {
super.onDestroy()
if (!isChangingConfigurations) { //activity won't be recreated
closeVolumeOnDestroy()
}
}
override fun onPause() {
super.onPause()
if (!isChangingConfigurations){
if (isStartingActivity){
isStartingActivity = false
} else if (!usf_keep_open){
finish()
}
taskScope.cancel()
}
}
override fun onResume() {
super.onResume()
if (isCreating){
isCreating = false
if (app.isStartingExternalApp) {
ExternalProvider.removeFilesAsync(this)
}
if (encryptedVolume.isClosed()) {
finish()
} else {
if (encryptedVolume.isClosed()) {
finish()
} else {
isStartingActivity = false
ExternalProvider.removeFilesAsync(this)
setCurrentPath(currentDirectoryPath)
}
setCurrentPath(currentDirectoryPath)
}
}
}

View File

@ -6,6 +6,7 @@ import android.net.Uri
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import com.google.android.material.floatingactionbutton.FloatingActionButton
@ -60,9 +61,7 @@ class ExplorerActivity : BaseExplorerActivity() {
}
if (operationFiles.size > 0){
checkPathOverwrite(operationFiles, currentDirectoryPath) { items ->
if (items == null) {
remoteEncryptedVolume.close()
} else {
if (items != null) {
// stop loading thumbnails while writing files
explorerAdapter.loadThumbnails = false
taskScope.launch {
@ -78,12 +77,9 @@ class ExplorerActivity : BaseExplorerActivity() {
}
explorerAdapter.loadThumbnails = true
setCurrentPath(currentDirectoryPath)
remoteEncryptedVolume.close()
}
}
}
} else {
remoteEncryptedVolume.close()
}
}
}
@ -169,6 +165,16 @@ class ExplorerActivity : BaseExplorerActivity() {
override fun init() {
super.init()
onBackPressedDispatcher.addCallback(this) {
if (currentItemAction != ItemsActions.NONE) {
cancelItemAction()
invalidateOptionsMenu()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
}
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
if (currentItemAction != ItemsActions.NONE){
openDialogCreateFolder()
@ -189,15 +195,14 @@ class ExplorerActivity : BaseExplorerActivity() {
val intent = Intent(this, MainActivity::class.java)
intent.action = "pick"
intent.putExtra("volume", encryptedVolume)
isStartingActivity = true
pickFromOtherVolumes.launch(intent)
}
"importFiles" -> {
isStartingActivity = true
app.isStartingExternalApp = true
pickFiles.launch(arrayOf("*/*"))
}
"importFolder" -> {
isStartingActivity = true
app.isStartingExternalApp = true
pickImportDirectory.launch(null)
}
"createFile" -> {
@ -212,7 +217,6 @@ class ExplorerActivity : BaseExplorerActivity() {
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("path", currentDirectoryPath)
intent.putExtra("volume", encryptedVolume)
isStartingActivity = true
startActivity(intent)
}
}
@ -257,6 +261,7 @@ class ExplorerActivity : BaseExplorerActivity() {
val result = super.onCreateOptionsMenu(menu)
if (currentItemAction != ItemsActions.NONE) {
menu.findItem(R.id.validate).isVisible = true
menu.findItem(R.id.lock).isVisible = false
menu.findItem(R.id.close).isVisible = false
supportActionBar?.setDisplayHomeAsUpEnabled(true)
} else {
@ -405,13 +410,13 @@ class ExplorerActivity : BaseExplorerActivity() {
for (i in explorerAdapter.selectedItems) {
paths.add(explorerElements[i].fullPath)
}
isStartingActivity = true
app.isStartingExternalApp = true
ExternalProvider.share(this, themeValue, encryptedVolume, paths)
unselectAll()
true
</