diff --git a/BUILD.md b/BUILD.md
index 3a28ed1..2c65b92 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -52,7 +52,7 @@ $ wget https://openssl.org/source/openssl-3.3.1.tar.gz
```
Verify OpenSSL signature:
```
-$ https://openssl.org/source/openssl-3.3.1.tar.gz.asc
+$ wget https://openssl.org/source/openssl-3.3.1.tar.gz.asc
$ gpg --verify openssl-3.3.1.tar.gz.asc openssl-3.3.1.tar.gz
```
Continue **ONLY** if the signature is **VALID**.
diff --git a/TODO.md b/TODO.md
index 288c4ab..d07e7ed 100644
--- a/TODO.md
+++ b/TODO.md
@@ -8,7 +8,8 @@ Here's a list of features that it would be nice to have in DroidFS. As this is a
## UX
- File associations editor
-- Optional discovery before file operations
+- Discovery before exporting
+- Making discovery before file operations optional
- Modifiable CryFS scrypt parameters
- Alert dialog showing details of file operations
- Internal file browser to select volumes
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 502260f..b9a382f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -58,10 +58,11 @@
-
+
+
-
+
diff --git a/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt
index f595c48..83f9be2 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt
@@ -4,6 +4,7 @@ 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
@@ -12,7 +13,7 @@ open class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- sharedPrefs = (application as VolumeManagerApp).sharedPreferences
+ sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
theme = Theme.fromSharedPrefs(sharedPrefs)
if (applyCustomTheme) {
setTheme(theme.toResourceId())
diff --git a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
index 0c7977f..832f24d 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
@@ -7,7 +7,10 @@ import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Size
-import android.view.*
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.Surface
+import android.view.View
import android.view.animation.Animation
import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
@@ -42,8 +45,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
-import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
+import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.video_recording.AsynchronousSeekableWriter
import sushi.hardcore.droidfs.video_recording.FFmpegMuxer
import sushi.hardcore.droidfs.video_recording.SeekableWriter
@@ -52,7 +55,9 @@ import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Date
+import java.util.Locale
+import java.util.Random
import java.util.concurrent.Executor
import kotlin.math.pow
import kotlin.math.sqrt
@@ -113,7 +118,10 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
- encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
+ encryptedVolume = (application as VolumeManagerApp).volumeManager.getVolume(
+ intent.getIntExtra("volumeId", -1)
+ )!!
+ finishOnClose(encryptedVolume)
outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -577,11 +585,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
override fun onResume() {
super.onResume()
- if (encryptedVolume.isClosed()) {
- finish()
- } else {
- sensorOrientationListener.addListener(this)
- }
+ sensorOrientationListener.addListener(this)
}
override fun onOrientationChange(newOrientation: Int) {
diff --git a/app/src/main/java/sushi/hardcore/droidfs/ClosingService.kt b/app/src/main/java/sushi/hardcore/droidfs/ClosingService.kt
new file mode 100644
index 0000000..c6303c0
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/ClosingService.kt
@@ -0,0 +1,20 @@
+package sushi.hardcore.droidfs
+
+import android.app.Service
+import android.content.Intent
+
+/**
+ * Dummy background service listening for application task removal in order to
+ * close all volumes still open on quit.
+ *
+ * Should only be running when usfBackground is enabled AND usfKeepOpen is disabled.
+ */
+class ClosingService : Service() {
+ override fun onBind(intent: Intent) = null
+
+ override fun onTaskRemoved(rootIntent: Intent) {
+ super.onTaskRemoved(rootIntent)
+ (application as VolumeManagerApp).volumeManager.closeAll()
+ stopSelf()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/KeepAliveService.kt b/app/src/main/java/sushi/hardcore/droidfs/KeepAliveService.kt
new file mode 100644
index 0000000..4097aa7
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/KeepAliveService.kt
@@ -0,0 +1,120 @@
+package sushi.hardcore.droidfs
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.Parcel
+import android.os.Parcelable
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.IntentCompat
+
+class KeepAliveService: Service() {
+ internal class NotificationDetails(
+ val channel: String,
+ val title: String,
+ val text: String,
+ val action: NotificationAction,
+ ) : Parcelable {
+ internal class NotificationAction(
+ val icon: Int,
+ val title: String,
+ val action: String,
+ )
+
+ constructor(parcel: Parcel) : this(
+ parcel.readString()!!,
+ parcel.readString()!!,
+ parcel.readString()!!,
+ NotificationAction(
+ parcel.readInt(),
+ parcel.readString()!!,
+ parcel.readString()!!,
+ )
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ with (parcel) {
+ writeString(channel)
+ writeString(title)
+ writeString(text)
+ writeInt(action.icon)
+ writeString(action.title)
+ writeString(action.action)
+ }
+ }
+
+ override fun describeContents() = 0
+
+ companion object CREATOR : Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel) = NotificationDetails(parcel)
+ override fun newArray(size: Int) = arrayOfNulls(size)
+ }
+
+ }
+
+ companion object {
+ const val ACTION_START = "start"
+
+ /**
+ * If [startForeground] is called before notification permission is granted,
+ * the notification won't appear.
+ *
+ * This action can be used once the permission is granted, to make the service
+ * call [startForeground] again in order to properly show the notification.
+ */
+ const val ACTION_FOREGROUND = "foreground"
+ const val NOTIFICATION_CHANNEL_ID = "KeepAlive"
+ }
+
+ private val notificationManager by lazy {
+ NotificationManagerCompat.from(this)
+ }
+ private var notification: Notification? = null
+
+ override fun onBind(intent: Intent?) = null
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ if (intent.action == ACTION_START) {
+ val notificationDetails = IntentCompat.getParcelableExtra(intent, "notification", NotificationDetails::class.java)!!
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.createNotificationChannel(
+ NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ notificationDetails.channel,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ )
+ }
+ notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_notification)
+ .setContentTitle(notificationDetails.title)
+ .setContentText(notificationDetails.text)
+ .addAction(NotificationCompat.Action(
+ notificationDetails.action.icon,
+ notificationDetails.action.title,
+ PendingIntent.getBroadcast(
+ this,
+ 0,
+ Intent(this, NotificationBroadcastReceiver::class.java).apply {
+ action = notificationDetails.action.action
+ },
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ ))
+ .build()
+ }
+ ServiceCompat.startForeground(this, startId, notification!!, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC // is there a better use case flag?
+ } else {
+ 0
+ })
+ return START_NOT_STICKY
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
index be3fb76..5495262 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
@@ -1,12 +1,8 @@
package sushi.hardcore.droidfs
-import android.content.ComponentName
-import android.content.Context
import android.content.Intent
-import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
-import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -131,7 +127,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
}
- startService(Intent(this, WiperService::class.java))
FileOperationService.bind(this) {
fileOperationService = it
}
@@ -184,9 +179,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
private fun unselect(position: Int) {
- volumeAdapter.selectedItems.remove(position)
- volumeAdapter.onVolumeChanged(position)
- onSelectionChanged(0) // unselect() is always called when only one element is selected
+ volumeAdapter.unselect(position)
invalidateOptionsMenu()
}
@@ -285,7 +278,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
R.id.delete_password_hash -> {
for (i in volumeAdapter.selectedItems) {
if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
- volumeAdapter.onVolumeChanged(i)
+ volumeAdapter.onVolumeDataChanged(i)
}
unselectAll(false)
true
@@ -475,6 +468,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (success) {
volumeDatabase.renameVolume(volume, newDBName)
VolumeProvider.notifyRootsChanged(this)
+ volumeAdapter.onVolumeDataChanged(position)
unselect(position)
if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) {
diff --git a/app/src/main/java/sushi/hardcore/droidfs/NotificationBroadcastReceiver.kt b/app/src/main/java/sushi/hardcore/droidfs/NotificationBroadcastReceiver.kt
new file mode 100644
index 0000000..12c88fb
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/NotificationBroadcastReceiver.kt
@@ -0,0 +1,23 @@
+package sushi.hardcore.droidfs
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import sushi.hardcore.droidfs.file_operations.FileOperationService
+
+class NotificationBroadcastReceiver: BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ FileOperationService.ACTION_CANCEL -> {
+ intent.getBundleExtra("bundle")?.let { bundle ->
+ // TODO: use peekService instead?
+ val binder = (bundle.getBinder("binder") as FileOperationService.LocalBinder?)
+ binder?.getService()?.cancelOperation(bundle.getInt("taskId"))
+ }
+ }
+ VolumeManagerApp.ACTION_CLOSE_ALL_VOLUMES -> {
+ (context.applicationContext as VolumeManagerApp).volumeManager.closeAll()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
index 35cfa6b..c10c3f4 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
@@ -8,21 +8,23 @@ import android.os.Bundle
import android.text.InputType
import android.view.MenuItem
import android.widget.Toast
+import androidx.core.content.ContextCompat
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreference
import androidx.preference.SwitchPreferenceCompat
-import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
+import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
class SettingsActivity : BaseActivity() {
+ private val notificationPermissionHelper = AndroidUtils.NotificationPermissionHelper(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -169,20 +171,23 @@ class SettingsActivity : BaseActivity() {
true
}
}
+ val switchBackground = findPreference("usf_background")!!
val switchKeepOpen = findPreference("usf_keep_open")!!
val switchExternalOpen = findPreference("usf_open")!!
val switchExpose = findPreference("usf_expose")!!
val switchSafWrite = findPreference("usf_saf_write")!!
- fun updateView(usfOpen: Boolean? = null, usfKeepOpen: Boolean? = null, usfExpose: Boolean? = null) {
- val usfKeepOpen = usfKeepOpen ?: switchKeepOpen.isChecked
- switchExpose.isEnabled = usfKeepOpen
- switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfKeepOpen && usfExpose ?: switchExpose.isChecked)
+ fun updateView(usfOpen: Boolean? = null, usfBackground: Boolean? = null, usfExpose: Boolean? = null) {
+ val usfBackground = usfBackground ?: switchBackground.isChecked
+ switchKeepOpen.isEnabled = usfBackground
+ switchExpose.isEnabled = usfBackground
+ switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfBackground && usfExpose ?: switchExpose.isChecked)
}
updateView()
- switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
- updateView(usfKeepOpen = checked as Boolean)
+ switchBackground.setOnPreferenceChangeListener { _, checked ->
+ updateView(usfBackground = checked as Boolean)
+ switchKeepOpen.isChecked = switchKeepOpen.isChecked && checked
true
}
switchExternalOpen.setOnPreferenceChangeListener { _, checked ->
@@ -190,17 +195,25 @@ class SettingsActivity : BaseActivity() {
true
}
switchExpose.setOnPreferenceChangeListener { _, checked ->
- VolumeProvider.usfExpose = checked as Boolean
- updateView(usfExpose = checked)
+ updateView(usfExpose = checked as Boolean)
VolumeProvider.notifyRootsChanged(requireContext())
true
}
- switchSafWrite.setOnPreferenceChangeListener { _, checked ->
- VolumeProvider.usfSafWrite = checked as Boolean
- TemporaryFileProvider.usfSafWrite = checked
+
+ switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
+ if (checked as Boolean) {
+ (requireActivity() as SettingsActivity).notificationPermissionHelper.askAndRun {
+ requireContext().let {
+ if (AndroidUtils.isServiceRunning(it, KeepAliveService::class.java)) {
+ ContextCompat.startForegroundService(it, Intent(it, KeepAliveService::class.java).apply {
+ action = KeepAliveService.ACTION_FOREGROUND
+ })
+ }
+ }
+ }
+ }
true
}
-
findPreference("export_method")!!.setOnPreferenceChangeListener { _, newValue ->
if (newValue as String == "memory" && !Compat.isMemFileSupported()) {
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
index 6a9b5ec..166d673 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
@@ -7,8 +7,14 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
+import sushi.hardcore.droidfs.util.Observable
+
+class VolumeManager(private val context: Context): Observable() {
+ interface Observer {
+ fun onVolumeStateChanged(volume: VolumeData) {}
+ fun onAllVolumesClosed() {}
+ }
-class VolumeManager(private val context: Context) {
private var id = 0
private val volumes = HashMap()
private val volumesData = HashMap()
@@ -17,6 +23,7 @@ class VolumeManager(private val context: Context) {
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume
volumesData[data] = id
+ observers.forEach { it.onVolumeStateChanged(data) }
VolumeProvider.notifyRootsChanged(context)
return id++
}
@@ -37,6 +44,8 @@ class VolumeManager(private val context: Context) {
return volumesData.map { (data, id) -> Pair(id, data) }
}
+ fun getVolumeCount() = volumes.size
+
fun getCoroutineScope(volumeId: Int): CoroutineScope {
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
}
@@ -44,9 +53,10 @@ class VolumeManager(private val context: Context) {
fun closeVolume(id: Int) {
volumes.remove(id)?.let { volume ->
scopes[id]?.cancel()
- volume.close()
- volumesData.filter { it.value == id }.forEach {
- volumesData.remove(it.key)
+ volume.closeVolume()
+ volumesData.filter { it.value == id }.forEach { entry ->
+ volumesData.remove(entry.key)
+ observers.forEach { it.onVolumeStateChanged(entry.key) }
}
VolumeProvider.notifyRootsChanged(context)
}
@@ -55,10 +65,11 @@ class VolumeManager(private val context: Context) {
fun closeAll() {
volumes.forEach {
scopes[it.key]?.cancel()
- it.value.close()
+ it.value.closeVolume()
}
volumes.clear()
volumesData.clear()
+ observers.forEach { it.onAllVolumesClosed() }
VolumeProvider.notifyRootsChanged(context)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
index 6e49652..5769e82 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
@@ -1,40 +1,88 @@
package sushi.hardcore.droidfs
import android.app.Application
-import android.content.SharedPreferences
+import android.content.Intent
+import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
-import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
+import sushi.hardcore.droidfs.util.AndroidUtils
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object {
- private const val USF_KEEP_OPEN_KEY = "usf_keep_open"
+ const val ACTION_CLOSE_ALL_VOLUMES = "close_all"
}
- lateinit var sharedPreferences: SharedPreferences
- private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
- if (key == USF_KEEP_OPEN_KEY) {
- reloadUsfKeepOpen()
- }
+ private val closingServiceIntent by lazy {
+ Intent(this, ClosingService::class.java)
}
- private var usfKeepOpen = false
+ private val keepAliveServiceStartIntent by lazy {
+ Intent(this, KeepAliveService::class.java).apply {
+ action = KeepAliveService.ACTION_START
+ }.putExtra(
+ "notification", KeepAliveService.NotificationDetails(
+ "KeepAlive",
+ getString(R.string.keep_alive_notification_title),
+ getString(R.string.keep_alive_notification_text),
+ KeepAliveService.NotificationDetails.NotificationAction(
+ R.drawable.icon_lock,
+ getString(R.string.close_all),
+ ACTION_CLOSE_ALL_VOLUMES,
+ )
+ )
+ )
+ }
+ private val usfBackgroundDelegate = AndroidUtils.LiveBooleanPreference("usf_background", false) { _ ->
+ updateServicesStates()
+ }
+ private val usfBackground by usfBackgroundDelegate
+ private val usfKeepOpenDelegate = AndroidUtils.LiveBooleanPreference("usf_keep_open", false) { _ ->
+ updateServicesStates()
+ }
+ private val usfKeepOpen by usfKeepOpenDelegate
var isExporting = false
var isStartingExternalApp = false
- val volumeManager = VolumeManager(this)
+ val volumeManager = VolumeManager(this).also {
+ it.observe(object : VolumeManager.Observer {
+ override fun onVolumeStateChanged(volume: VolumeData) {
+ updateServicesStates()
+ }
+
+ override fun onAllVolumesClosed() {
+ stopKeepAliveService()
+ // closingService should not be running when this callback is triggered
+ }
+ })
+ }
override fun onCreate() {
super.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
- sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this).apply {
- registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
- }
- reloadUsfKeepOpen()
+ AndroidUtils.LiveBooleanPreference.init(this, usfBackgroundDelegate, usfKeepOpenDelegate)
}
- private fun reloadUsfKeepOpen() {
- usfKeepOpen = sharedPreferences.getBoolean(USF_KEEP_OPEN_KEY, false)
+ fun updateServicesStates() {
+ if (usfBackground && volumeManager.getVolumeCount() > 0) {
+ if (usfKeepOpen) {
+ stopService(closingServiceIntent)
+ if (!AndroidUtils.isServiceRunning(this@VolumeManagerApp, KeepAliveService::class.java)) {
+ ContextCompat.startForegroundService(this, keepAliveServiceStartIntent)
+ }
+ } else {
+ stopKeepAliveService()
+ if (!AndroidUtils.isServiceRunning(this@VolumeManagerApp, ClosingService::class.java)) {
+ startService(closingServiceIntent)
+ }
+ }
+ } else {
+ stopService(closingServiceIntent)
+ stopKeepAliveService()
+ }
+ }
+
+ private fun stopKeepAliveService() {
+ stopService(Intent(this, KeepAliveService::class.java))
}
override fun onResume(owner: LifecycleOwner) {
@@ -43,10 +91,10 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
if (!isStartingExternalApp) {
- if (!usfKeepOpen) {
+ if (!usfBackground) {
volumeManager.closeAll()
}
- if (!usfKeepOpen || !isExporting) {
+ if (!usfBackground || !isExporting) {
TemporaryFileProvider.instance.wipe()
}
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt
index 6dba779..6030ba2 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt
@@ -211,7 +211,7 @@ class VolumeOpener(
private var isClosed = false
override fun onFailed(pending: Boolean) {
if (!isClosed) {
- encryptedVolume.close()
+ encryptedVolume.closeVolume()
isClosed = true
}
Arrays.fill(returnedHash.value!!, 0)
diff --git a/app/src/main/java/sushi/hardcore/droidfs/WiperService.kt b/app/src/main/java/sushi/hardcore/droidfs/WiperService.kt
deleted file mode 100644
index 3394137..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/WiperService.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package sushi.hardcore.droidfs
-
-import android.app.Service
-import android.content.Intent
-import android.os.IBinder
-
-class WiperService : Service() {
- override fun onBind(intent: Intent): IBinder? {
- return null
- }
-
- override fun onTaskRemoved(rootIntent: Intent) {
- super.onTaskRemoved(rootIntent)
- (application as VolumeManagerApp).volumeManager.closeAll()
- stopSelf()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/SelectableAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/SelectableAdapter.kt
index 32cabb2..2b4430e 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/adapters/SelectableAdapter.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/SelectableAdapter.kt
@@ -40,6 +40,12 @@ abstract class SelectableAdapter(private val onSelectionChanged: (Int) -> Uni
return true
}
+ fun unselect(position: Int) {
+ selectedItems.remove(position)
+ onSelectionChanged(selectedItems.size)
+ notifyItemChanged(position)
+ }
+
fun selectAll() {
for (i in getItems().indices) {
if (!selectedItems.contains(i) && isSelectable(i)) {
diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt
index 8aa433a..b735d69 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt
@@ -29,6 +29,16 @@ class VolumeAdapter(
init {
reloadVolumes()
+ volumeManager.observe(object : VolumeManager.Observer {
+ override fun onVolumeStateChanged(volume: VolumeData) {
+ notifyItemChanged(volumes.indexOf(volume))
+ }
+
+ @SuppressLint("NotifyDataSetChanged")
+ override fun onAllVolumesClosed() {
+ notifyDataSetChanged()
+ }
+ })
}
interface Listener {
@@ -66,7 +76,7 @@ class VolumeAdapter(
false
}
- fun onVolumeChanged(position: Int) {
+ fun onVolumeDataChanged(position: Int) {
reloadVolumes()
notifyItemChanged(position)
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
index 4d227cb..d4ed07c 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
@@ -10,7 +10,6 @@ import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
import android.util.Log
import android.webkit.MimeTypeMap
-import androidx.preference.PreferenceManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -18,6 +17,7 @@ import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
+import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.util.UUID
@@ -36,9 +36,10 @@ class TemporaryFileProvider : ContentProvider() {
lateinit var instance: TemporaryFileProvider
private set
- var usfSafWrite = false
}
+ private val usfSafWriteDelegate = AndroidUtils.LiveBooleanPreference("usf_saf_write", false)
+ private val usfSafWrite by usfSafWriteDelegate
private lateinit var volumeManager: VolumeManager
lateinit var encryptedFileProvider: EncryptedFileProvider
private val files = HashMap()
@@ -46,8 +47,7 @@ class TemporaryFileProvider : ContentProvider() {
override fun onCreate(): Boolean {
return context?.let {
volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager
- usfSafWrite =
- PreferenceManager.getDefaultSharedPreferences(it).getBoolean("usf_saf_write", false)
+ usfSafWriteDelegate.init(it)
encryptedFileProvider = EncryptedFileProvider(it)
instance = this
val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it)
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt
index 60a2d35..1e57574 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt
@@ -18,6 +18,7 @@ import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
+import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
@@ -40,23 +41,23 @@ class VolumeProvider: DocumentsProvider() {
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
)
- var usfExpose = false
- var usfSafWrite = false
fun notifyRootsChanged(context: Context) {
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
}
}
+ private val usfExposeDelegate = AndroidUtils.LiveBooleanPreference("usf_expose", false)
+ private val usfExpose by usfExposeDelegate
+ private val usfSafWriteDelegate = AndroidUtils.LiveBooleanPreference("usf_saf_write", false)
+ private val usfSafWrite by usfSafWriteDelegate
private lateinit var volumeManager: VolumeManager
private val volumes = HashMap>()
private lateinit var encryptedFileProvider: EncryptedFileProvider
override fun onCreate(): Boolean {
val context = (context ?: return false)
- val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
- usfExpose = sharedPreferences.getBoolean("usf_expose", false)
- usfSafWrite = sharedPreferences.getBoolean("usf_saf_write", false)
+ AndroidUtils.LiveBooleanPreference.init(context, usfExposeDelegate, usfSafWriteDelegate)
volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager
encryptedFileProvider = EncryptedFileProvider(context)
return true
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
index 2dd734c..cb8a5bb 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
@@ -1,12 +1,8 @@
package sushi.hardcore.droidfs.explorers
-import android.content.ComponentName
-import android.content.Context
import android.content.Intent
-import android.content.ServiceConnection
import android.net.Uri
import android.os.Bundle
-import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.view.View
@@ -15,7 +11,6 @@ import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
-import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -27,7 +22,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
@@ -55,6 +49,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.UIUtils
+import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@@ -100,7 +95,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
usf_open = sharedPrefs.getBoolean("usf_open", false)
volumeName = intent.getStringExtra("volumeName") ?: ""
volumeId = intent.getIntExtra("volumeId", -1)
- encryptedVolume = app.volumeManager.getVolume(volumeId)!!
+ encryptedVolume = app.volumeManager.getVolume(volumeId)!!.also { finishOnClose(it) }
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@@ -199,7 +194,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private fun startFileViewer(cls: Class<*>, filePath: String) {
val intent = Intent(this, cls).apply {
putExtra("path", filePath)
- putExtra("volume", encryptedVolume)
+ putExtra("volumeId", volumeId)
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
}
startActivity(intent)
@@ -679,10 +674,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (app.isStartingExternalApp) {
TemporaryFileProvider.instance.wipe()
}
- if (encryptedVolume.isClosed()) {
- finish()
- } else {
- setCurrentPath(currentDirectoryPath)
- }
+ setCurrentPath(currentDirectoryPath)
}
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
index a552c52..c59316e 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
@@ -181,7 +181,6 @@ class ExplorerActivity : BaseExplorerActivity() {
"importFromOtherVolumes" -> {
val intent = Intent(this, MainActivity::class.java)
intent.action = "pick"
- intent.putExtra("volume", encryptedVolume)
pickFromOtherVolumes.launch(intent)
}
"importFiles" -> {
@@ -205,7 +204,7 @@ class ExplorerActivity : BaseExplorerActivity() {
"camera" -> {
val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("path", currentDirectoryPath)
- intent.putExtra("volume", encryptedVolume)
+ intent.putExtra("volumeId", volumeId)
startActivity(intent)
}
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
index d953dc3..7ed2cc2 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt
@@ -1,7 +1,5 @@
package sushi.hardcore.droidfs.file_operations
-import android.Manifest
-import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -11,7 +9,6 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
-import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Binder
@@ -21,7 +18,6 @@ import android.os.IBinder
import android.provider.Settings
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
@@ -39,12 +35,14 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants
+import sushi.hardcore.droidfs.NotificationBroadcastReceiver
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
+import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper
@@ -91,32 +89,11 @@ class FileOperationService : Service() {
* If multiple activities bind simultaneously, only the latest one will be used by the service.
*/
fun bind(activity: BaseActivity, onBound: (FileOperationService) -> Unit) {
- var service: FileOperationService? = null
- val launcher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
- if (granted) {
- service!!.processPendingTask()
- } else {
- CustomAlertDialogBuilder(activity, activity.theme)
- .setTitle(R.string.warning)
- .setMessage(R.string.notification_denied_msg)
- .setPositiveButton(R.string.settings) { _, _ ->
- activity.startActivity(
- Intent(
- Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
- Uri.fromParts("package", activity.packageName, null)
- )
- )
- }
- .setNegativeButton(R.string.later, null)
- .setOnDismissListener { service!!.processPendingTask() }
- .show()
- }
- }
+ val helper = AndroidUtils.NotificationPermissionHelper(activity)
activity.bindService(Intent(activity, FileOperationService::class.java), object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
onBound((binder as FileOperationService.LocalBinder).getService().also {
- it.notificationPermissionLauncher = launcher
- service = it
+ it.notificationPermissionHelper = helper
})
}
override fun onServiceDisconnected(arg0: ComponentName) {}
@@ -128,7 +105,7 @@ class FileOperationService : Service() {
private val binder = LocalBinder()
private lateinit var volumeManger: VolumeManager
private var serviceScope = MainScope()
- private lateinit var notificationPermissionLauncher: ActivityResultLauncher
+ private lateinit var notificationPermissionHelper: AndroidUtils.NotificationPermissionHelper
private var askForNotificationPermission = true
private lateinit var notificationManager: NotificationManagerCompat
private val notifications = HashMap()
@@ -315,18 +292,30 @@ class FileOperationService : Service() {
continuation.resume(Pair(taskId, job))
}
pendingTask = task
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (
- ContextCompat.checkSelfPermission(
- this,
- Manifest.permission.POST_NOTIFICATIONS
- ) != PackageManager.PERMISSION_GRANTED
- && askForNotificationPermission
- ) {
- notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- askForNotificationPermission = false // only ask once per service instance
- return@suspendCoroutine
+ if (askForNotificationPermission) {
+ notificationPermissionHelper.askAndRun { granted ->
+ if (granted) {
+ processPendingTask()
+ } else {
+ CustomAlertDialogBuilder(notificationPermissionHelper.activity, notificationPermissionHelper.activity.theme)
+ .setTitle(R.string.warning)
+ .setMessage(R.string.notification_denied_msg)
+ .setPositiveButton(R.string.settings) { _, _ ->
+ (application as VolumeManagerApp).isStartingExternalApp = true
+ notificationPermissionHelper.activity.startActivity(
+ Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", packageName, null)
+ )
+ )
+ }
+ .setNegativeButton(R.string.later, null)
+ .setOnDismissListener { processPendingTask() }
+ .show()
+ }
}
+ askForNotificationPermission = false // only ask once per service instance
+ return@suspendCoroutine
}
processPendingTask()
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt
deleted file mode 100644
index ae0ebca..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/NotificationBroadcastReceiver.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package sushi.hardcore.droidfs.file_operations
-
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-
-class NotificationBroadcastReceiver: BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- if (intent.action == FileOperationService.ACTION_CANCEL) {
- intent.getBundleExtra("bundle")?.let { bundle ->
- // TODO: use peekService instead?
- val binder = (bundle.getBinder("binder") as FileOperationService.LocalBinder?)
- binder?.getService()?.cancelOperation(bundle.getInt("taskId"))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt
index 4caee6d..f327606 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/FileViewerActivity.kt
@@ -18,10 +18,12 @@ import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.R
+import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
+import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
abstract class FileViewerActivity: BaseActivity() {
@@ -40,7 +42,10 @@ abstract class FileViewerActivity: BaseActivity() {
super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath)
- encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
+ encryptedVolume = (application as VolumeManagerApp).volumeManager.getVolume(
+ intent.getIntExtra("volumeId", -1)
+ )!!
+ finishOnClose(encryptedVolume)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask ->
@@ -173,11 +178,4 @@ abstract class FileViewerActivity: BaseActivity() {
protected fun goBackToExplorer() {
finish()
}
-
- override fun onResume() {
- super.onResume()
- if (encryptedVolume.isClosed()) {
- finish()
- }
- }
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt
index c97e9d4..2303c65 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/CryfsVolume.kt
@@ -1,6 +1,5 @@
package sushi.hardcore.droidfs.filesystems
-import android.os.Parcel
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
@@ -101,13 +100,6 @@ class CryfsVolume(private val fusePtr: Long): EncryptedVolume() {
}
}
- constructor(parcel: Parcel) : this(parcel.readLong())
-
- override fun writeToParcel(parcel: Parcel, flags: Int) = with(parcel) {
- writeByte(CRYFS_VOLUME_TYPE)
- writeLong(fusePtr)
- }
-
override fun openFileReadMode(path: String): Long {
return nativeOpen(fusePtr, path, 0)
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
index 6e3a4a8..3d9bee9 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
@@ -2,18 +2,21 @@ package sushi.hardcore.droidfs.filesystems
import android.content.Context
import android.net.Uri
-import android.os.Parcel
-import android.os.Parcelable
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.ObjRef
+import sushi.hardcore.droidfs.util.Observable
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
-abstract class EncryptedVolume: Parcelable {
+abstract class EncryptedVolume: Observable() {
+
+ interface Observer {
+ fun onClose()
+ }
class InitResult(
val errorCode: Int,
@@ -35,18 +38,6 @@ abstract class EncryptedVolume: Parcelable {
const val GOCRYPTFS_VOLUME_TYPE: Byte = 0
const val CRYFS_VOLUME_TYPE: Byte = 1
- @JvmField
- val CREATOR = object : Parcelable.Creator {
- override fun createFromParcel(parcel: Parcel): EncryptedVolume {
- return when (parcel.readByte()) {
- GOCRYPTFS_VOLUME_TYPE -> GocryptfsVolume(parcel)
- CRYFS_VOLUME_TYPE -> CryfsVolume(parcel)
- else -> throw invalidVolumeType()
- }
- }
- override fun newArray(size: Int) = arrayOfNulls(size)
- }
-
/**
* Get the type of a volume.
*
@@ -92,8 +83,6 @@ abstract class EncryptedVolume: Parcelable {
}
}
- override fun describeContents() = 0
-
abstract fun openFileReadMode(path: String): Long
abstract fun openFileWriteMode(path: String): Long
abstract fun read(fileHandle: Long, fileOffset: Long, buffer: ByteArray, dstOffset: Long, length: Long): Int
@@ -107,9 +96,14 @@ abstract class EncryptedVolume: Parcelable {
abstract fun rmdir(path: String): Boolean
abstract fun getAttr(path: String): Stat?
abstract fun rename(srcPath: String, dstPath: String): Boolean
- abstract fun close()
+ protected abstract fun close()
abstract fun isClosed(): Boolean
+ fun closeVolume() {
+ observers.forEach { it.onClose() }
+ close()
+ }
+
fun pathExists(path: String): Boolean {
return getAttr(path) != null
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt
index 19096a5..f9623a2 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/GocryptfsVolume.kt
@@ -1,6 +1,5 @@
package sushi.hardcore.droidfs.filesystems
-import android.os.Parcel
import android.util.Log
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
@@ -100,8 +99,6 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
}
}
- constructor(parcel: Parcel) : this(parcel.readInt())
-
override fun openFileReadMode(path: String): Long {
return native_open_read_mode(sessionID, path).toLong()
}
@@ -122,11 +119,6 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
return native_get_attr(sessionID, path)
}
- override fun writeToParcel(parcel: Parcel, flags: Int) = with(parcel) {
- writeByte(GOCRYPTFS_VOLUME_TYPE)
- writeInt(sessionID)
- }
-
override fun close() {
native_close(sessionID)
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/AndroidUtils.kt b/app/src/main/java/sushi/hardcore/droidfs/util/AndroidUtils.kt
new file mode 100644
index 0000000..93edb0e
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/util/AndroidUtils.kt
@@ -0,0 +1,111 @@
+package sushi.hardcore.droidfs.util
+
+import android.Manifest
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.preference.PreferenceManager
+import kotlin.reflect.KProperty
+
+object AndroidUtils {
+ fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ @Suppress("DEPRECATION")
+ for (service in manager.getRunningServices(Int.MAX_VALUE)) {
+ if (serviceClass.name == service.service.className) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * A [Manifest.permission.POST_NOTIFICATIONS] permission helper.
+ *
+ * Must be initialized before [Activity.onCreate].
+ */
+ class NotificationPermissionHelper(val activity: A) {
+ private var listener: ((Boolean) -> Unit)? = null
+ private val launcher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
+ listener?.invoke(granted)
+ listener = null
+ }
+
+ /**
+ * Ask for notification permission if required and run the provided callback.
+ *
+ * The callback is run as soon as the user dismisses the permission dialog,
+ * no matter if the permission has been granted or not.
+ *
+ * If this function is called again before the user answered the dialog from the
+ * previous call, the previous callback won't be triggered.
+ *
+ * @param onDialogDismiss argument set to `true` if the permission is granted or
+ * not required, `false` otherwise
+ */
+ fun askAndRun(onDialogDismiss: (Boolean) -> Unit) {
+ assert(listener == null)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ listener = onDialogDismiss
+ launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ return
+ }
+ }
+ onDialogDismiss(true)
+ }
+ }
+
+ /**
+ * Property delegate mirroring the state of a boolean value in shared preferences.
+ *
+ * [init] **must** be called before accessing the delegated property.
+ */
+ class LiveBooleanPreference(
+ private val key: String,
+ private val defaultValue: Boolean = false,
+ private val onChange: ((value: Boolean) -> Unit)? = null
+ ) {
+ private lateinit var sharedPreferences: SharedPreferences
+ private var value = defaultValue
+ private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == this.key) {
+ reload()
+ onChange?.invoke(value)
+ }
+ }
+
+ fun init(context: Context) = init(PreferenceManager.getDefaultSharedPreferences(context))
+
+ fun init(sharedPreferences: SharedPreferences) {
+ this.sharedPreferences = sharedPreferences
+ reload()
+ sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+ }
+
+ private fun reload() {
+ value = sharedPreferences.getBoolean(key, defaultValue)
+ }
+
+ operator fun getValue(thisRef: Any, property: KProperty<*>) = value
+
+ companion object {
+ fun init(context: Context, vararg liveBooleanPreferences: LiveBooleanPreference) {
+ init(PreferenceManager.getDefaultSharedPreferences(context), *liveBooleanPreferences)
+ }
+
+ fun init(sharedPreferences: SharedPreferences, vararg liveBooleanPreferences: LiveBooleanPreference) {
+ for (i in liveBooleanPreferences) {
+ i.init(sharedPreferences)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/Observable.kt b/app/src/main/java/sushi/hardcore/droidfs/util/Observable.kt
new file mode 100644
index 0000000..d10ddff
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/util/Observable.kt
@@ -0,0 +1,21 @@
+package sushi.hardcore.droidfs.util
+
+import android.app.Activity
+import sushi.hardcore.droidfs.filesystems.EncryptedVolume
+
+abstract class Observable {
+ protected val observers = mutableListOf()
+
+ fun observe(observer: T) {
+ observers.add(observer)
+ }
+}
+
+fun Activity.finishOnClose(encryptedVolume: EncryptedVolume) {
+ encryptedVolume.observe(object : EncryptedVolume.Observer {
+ override fun onClose() {
+ finish()
+ // no need to remove observer as the EncryptedVolume will be destroyed
+ }
+ })
+}
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 209ce08..773ff07 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -73,7 +73,6 @@
السماح بلقطة شاشة
السماح بحفظ تجزئة كلمة المرور باستخدام بصمة الإصبع
إدارة مجلد التشفير
- إبقاء مجلد التشفير مفتوحاً عند الخروج من التطبيق
الميزات غير الآمنة
إدارة الميزات غير الآمنة
تمكين / تعطيل الميزات غير الآمنة
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index c2e65f9..8bcdc5c 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -74,7 +74,6 @@
Screenshots zulassen
Kennwort-Hash mit Fingerabdruck speichern können
Volumenverwaltung
- Volumen offen halten, wenn die App in den Hintergrund geht
Unsichere Funktionen
Sichere Funktionen verwalten
Aktivieren/Deaktivieren unsicherer Funktionen
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 0cb131f..2b320fb 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -74,7 +74,6 @@
Permitir capturas de pantalla
Permitir guardar el hash de la contraseña mediante la huella dactilar
Gestión del volumen
- Mantener el volumen abierto cuando la aplicación está en segundo plano
Características inseguras
Gestionar las características inseguras
Activar/desactivar funciones inseguras
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index ad08e07..ab0f80e 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -73,7 +73,6 @@
Permitir capturas da tela
Permitir salvar o hash da senha usando impressão digital
Gerenciador de volumes
- Mantenha o volume aberto quando o app ficar em segundo plano
Opções perigosas
Gerenciar opções perigosas
Alternar opções perigosas
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index e64c7a7..013aaab 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -71,7 +71,6 @@
Разрешить снимки экрана
Разрешить сохранение хеша пароля отпечатком пальца
Управление томом
- Оставлять том открытым, когда DroidFS в фоне
Небезопасные функции
Управление небезопасными функциями
Включить/отключить небезопасные функции
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 5476de0..1a4bb47 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -74,7 +74,6 @@
Ekran görüntüsü almaya izin ver
Parmak izi kullanılarak şifre hash değerinin kaydedilmesine izin ver
Birim yönetimi
- Uygulama arka plana geçtiğinde birimi açık tutun
Güvenli olmayan özellikler
Güvenli olmayan özellikleri yönetin
Güvenli olmayan özellikleri etkinleştirme/devre dışı bırakma
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index b3edc6f..aa7827f 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -75,7 +75,6 @@
允许截屏
允许通过指纹保存密码哈希
加密卷管理
- 当应用切入后台时已打开的卷不再自动锁上
以下功能会降低安全性
管理非安全功能
打开/关闭非安全功能
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 11a2e58..d8450fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -74,7 +74,6 @@
Allow screenshots
Allow saving password hash using fingerprint
Volume Management
- Keep volume open when the app goes in background
Unsafe Features
Manage unsafe features
Enable/Disable unsafe features
@@ -281,4 +280,11 @@
Logcat saved
Later
Notification permission has been denied. Background file operations won\'t be visible. You can change this in the app\'s permission settings.
+ Keep alive service
+ One or more volumes are kept open.
+ Close all
+ Disable volume auto-locking
+ Don\'t lock volumes when the app goes in background
+ Keep volumes open
+ Maintain the app always running in the background to keep volumes open
diff --git a/app/src/main/res/xml/unsafe_features_preferences.xml b/app/src/main/res/xml/unsafe_features_preferences.xml
index a336cb8..4ecc2e3 100644
--- a/app/src/main/res/xml/unsafe_features_preferences.xml
+++ b/app/src/main/res/xml/unsafe_features_preferences.xml
@@ -48,11 +48,19 @@
android:key="usf_fingerprint"
android:title="@string/usf_fingerprint" />
+
+
+ android:title="@string/usf_keep_open"
+ android:summary="@string/usf_keep_open_summary"/>