KeepAlive foreground service

This commit is contained in:
Matéo Duparc 2024-07-16 15:03:44 +02:00
parent 52a29b034c
commit 33d565bf22
Signed by untrusted user: hardcoresushi
GPG Key ID: AFE384344A45E13A
37 changed files with 522 additions and 209 deletions

View File

@ -52,7 +52,7 @@ $ wget https://openssl.org/source/openssl-3.3.1.tar.gz
``` ```
Verify OpenSSL signature: 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 $ gpg --verify openssl-3.3.1.tar.gz.asc openssl-3.3.1.tar.gz
``` ```
Continue **ONLY** if the signature is **VALID**. Continue **ONLY** if the signature is **VALID**.

View File

@ -8,7 +8,8 @@ Here's a list of features that it would be nice to have in DroidFS. As this is a
## UX ## UX
- File associations editor - File associations editor
- Optional discovery before file operations - Discovery before exporting
- Making discovery before file operations optional
- Modifiable CryFS scrypt parameters - Modifiable CryFS scrypt parameters
- Alert dialog showing details of file operations - Alert dialog showing details of file operations
- Internal file browser to select volumes - Internal file browser to select volumes

View File

@ -58,10 +58,11 @@
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" /> <activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
<activity android:name=".LogcatActivity"/> <activity android:name=".LogcatActivity"/>
<service android:name=".WiperService" android:exported="false" android:stopWithTask="false"/> <service android:name=".KeepAliveService" android:exported="false" android:foregroundServiceType="dataSync" />
<service android:name=".ClosingService" android:exported="false" android:stopWithTask="false"/>
<service android:name=".file_operations.FileOperationService" android:exported="false" android:foregroundServiceType="dataSync"/> <service android:name=".file_operations.FileOperationService" android:exported="false" android:foregroundServiceType="dataSync"/>
<receiver android:name=".file_operations.NotificationBroadcastReceiver" android:exported="false"> <receiver android:name=".NotificationBroadcastReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="file_operation_cancel"/> <action android:name="file_operation_cancel"/>
</intent-filter> </intent-filter>

View File

@ -4,6 +4,7 @@ import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
open class BaseActivity: AppCompatActivity() { open class BaseActivity: AppCompatActivity() {
protected lateinit var sharedPrefs: SharedPreferences protected lateinit var sharedPrefs: SharedPreferences
@ -12,7 +13,7 @@ open class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sharedPrefs = (application as VolumeManagerApp).sharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
theme = Theme.fromSharedPrefs(sharedPrefs) theme = Theme.fromSharedPrefs(sharedPrefs)
if (applyCustomTheme) { if (applyCustomTheme) {
setTheme(theme.toResourceId()) setTheme(theme.toResourceId())

View File

@ -7,7 +7,10 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.util.Size 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.Animation
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
@ -42,8 +45,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils 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.AsynchronousSeekableWriter
import sushi.hardcore.droidfs.video_recording.FFmpegMuxer import sushi.hardcore.droidfs.video_recording.FFmpegMuxer
import sushi.hardcore.droidfs.video_recording.SeekableWriter import sushi.hardcore.droidfs.video_recording.SeekableWriter
@ -52,7 +55,9 @@ import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat 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 java.util.concurrent.Executor
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt import kotlin.math.sqrt
@ -113,7 +118,10 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater) binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.hide() supportActionBar?.hide()
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!! encryptedVolume = (application as VolumeManagerApp).volumeManager.getVolume(
intent.getIntExtra("volumeId", -1)
)!!
finishOnClose(encryptedVolume)
outputDirectory = intent.getStringExtra("path")!! outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -577,12 +585,8 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (encryptedVolume.isClosed()) {
finish()
} else {
sensorOrientationListener.addListener(this) sensorOrientationListener.addListener(this)
} }
}
override fun onOrientationChange(newOrientation: Int) { override fun onOrientationChange(newOrientation: Int) {
val realOrientation = when (newOrientation) { val realOrientation = when (newOrientation) {

View File

@ -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()
}
}

View File

@ -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<NotificationDetails> {
override fun createFromParcel(parcel: Parcel) = NotificationDetails(parcel)
override fun newArray(size: Int) = arrayOfNulls<NotificationDetails>(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
}
}

View File

@ -1,12 +1,8 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -131,7 +127,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
} }
startService(Intent(this, WiperService::class.java))
FileOperationService.bind(this) { FileOperationService.bind(this) {
fileOperationService = it fileOperationService = it
} }
@ -184,9 +179,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
private fun unselect(position: Int) { private fun unselect(position: Int) {
volumeAdapter.selectedItems.remove(position) volumeAdapter.unselect(position)
volumeAdapter.onVolumeChanged(position)
onSelectionChanged(0) // unselect() is always called when only one element is selected
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -285,7 +278,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
R.id.delete_password_hash -> { R.id.delete_password_hash -> {
for (i in volumeAdapter.selectedItems) { for (i in volumeAdapter.selectedItems) {
if (volumeDatabase.removeHash(volumeAdapter.volumes[i])) if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
volumeAdapter.onVolumeChanged(i) volumeAdapter.onVolumeDataChanged(i)
} }
unselectAll(false) unselectAll(false)
true true
@ -475,6 +468,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (success) { if (success) {
volumeDatabase.renameVolume(volume, newDBName) volumeDatabase.renameVolume(volume, newDBName)
VolumeProvider.notifyRootsChanged(this) VolumeProvider.notifyRootsChanged(this)
volumeAdapter.onVolumeDataChanged(position)
unselect(position) unselect(position)
if (volume.name == volumeOpener.defaultVolumeName) { if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) { with (sharedPrefs.edit()) {

View File

@ -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()
}
}
}
}

View File

@ -8,21 +8,23 @@ import android.os.Bundle
import android.text.InputType import android.text.InputType
import android.view.MenuItem import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.content_providers.VolumeProvider import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.Compat import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
class SettingsActivity : BaseActivity() { class SettingsActivity : BaseActivity() {
private val notificationPermissionHelper = AndroidUtils.NotificationPermissionHelper(this)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -169,20 +171,23 @@ class SettingsActivity : BaseActivity() {
true true
} }
} }
val switchBackground = findPreference<SwitchPreference>("usf_background")!!
val switchKeepOpen = findPreference<SwitchPreference>("usf_keep_open")!! val switchKeepOpen = findPreference<SwitchPreference>("usf_keep_open")!!
val switchExternalOpen = findPreference<SwitchPreference>("usf_open")!! val switchExternalOpen = findPreference<SwitchPreference>("usf_open")!!
val switchExpose = findPreference<SwitchPreference>("usf_expose")!! val switchExpose = findPreference<SwitchPreference>("usf_expose")!!
val switchSafWrite = findPreference<SwitchPreference>("usf_saf_write")!! val switchSafWrite = findPreference<SwitchPreference>("usf_saf_write")!!
fun updateView(usfOpen: Boolean? = null, usfKeepOpen: Boolean? = null, usfExpose: Boolean? = null) { fun updateView(usfOpen: Boolean? = null, usfBackground: Boolean? = null, usfExpose: Boolean? = null) {
val usfKeepOpen = usfKeepOpen ?: switchKeepOpen.isChecked val usfBackground = usfBackground ?: switchBackground.isChecked
switchExpose.isEnabled = usfKeepOpen switchKeepOpen.isEnabled = usfBackground
switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfKeepOpen && usfExpose ?: switchExpose.isChecked) switchExpose.isEnabled = usfBackground
switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfBackground && usfExpose ?: switchExpose.isChecked)
} }
updateView() updateView()
switchKeepOpen.setOnPreferenceChangeListener { _, checked -> switchBackground.setOnPreferenceChangeListener { _, checked ->
updateView(usfKeepOpen = checked as Boolean) updateView(usfBackground = checked as Boolean)
switchKeepOpen.isChecked = switchKeepOpen.isChecked && checked
true true
} }
switchExternalOpen.setOnPreferenceChangeListener { _, checked -> switchExternalOpen.setOnPreferenceChangeListener { _, checked ->
@ -190,17 +195,25 @@ class SettingsActivity : BaseActivity() {
true true
} }
switchExpose.setOnPreferenceChangeListener { _, checked -> switchExpose.setOnPreferenceChangeListener { _, checked ->
VolumeProvider.usfExpose = checked as Boolean updateView(usfExpose = checked as Boolean)
updateView(usfExpose = checked)
VolumeProvider.notifyRootsChanged(requireContext()) VolumeProvider.notifyRootsChanged(requireContext())
true true
} }
switchSafWrite.setOnPreferenceChangeListener { _, checked ->
VolumeProvider.usfSafWrite = checked as Boolean switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
TemporaryFileProvider.usfSafWrite = 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 true
} }
findPreference<ListPreference>("export_method")!!.setOnPreferenceChangeListener { _, newValue -> findPreference<ListPreference>("export_method")!!.setOnPreferenceChangeListener { _, newValue ->
if (newValue as String == "memory" && !Compat.isMemFileSupported()) { if (newValue as String == "memory" && !Compat.isMemFileSupported()) {
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme) CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)

View File

@ -7,8 +7,14 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import sushi.hardcore.droidfs.content_providers.VolumeProvider import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.Observable
class VolumeManager(private val context: Context): Observable<VolumeManager.Observer>() {
interface Observer {
fun onVolumeStateChanged(volume: VolumeData) {}
fun onAllVolumesClosed() {}
}
class VolumeManager(private val context: Context) {
private var id = 0 private var id = 0
private val volumes = HashMap<Int, EncryptedVolume>() private val volumes = HashMap<Int, EncryptedVolume>()
private val volumesData = HashMap<VolumeData, Int>() private val volumesData = HashMap<VolumeData, Int>()
@ -17,6 +23,7 @@ class VolumeManager(private val context: Context) {
fun insert(volume: EncryptedVolume, data: VolumeData): Int { fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume volumes[id] = volume
volumesData[data] = id volumesData[data] = id
observers.forEach { it.onVolumeStateChanged(data) }
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootsChanged(context)
return id++ return id++
} }
@ -37,6 +44,8 @@ class VolumeManager(private val context: Context) {
return volumesData.map { (data, id) -> Pair(id, data) } return volumesData.map { (data, id) -> Pair(id, data) }
} }
fun getVolumeCount() = volumes.size
fun getCoroutineScope(volumeId: Int): CoroutineScope { fun getCoroutineScope(volumeId: Int): CoroutineScope {
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it } 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) { fun closeVolume(id: Int) {
volumes.remove(id)?.let { volume -> volumes.remove(id)?.let { volume ->
scopes[id]?.cancel() scopes[id]?.cancel()
volume.close() volume.closeVolume()
volumesData.filter { it.value == id }.forEach { volumesData.filter { it.value == id }.forEach { entry ->
volumesData.remove(it.key) volumesData.remove(entry.key)
observers.forEach { it.onVolumeStateChanged(entry.key) }
} }
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootsChanged(context)
} }
@ -55,10 +65,11 @@ class VolumeManager(private val context: Context) {
fun closeAll() { fun closeAll() {
volumes.forEach { volumes.forEach {
scopes[it.key]?.cancel() scopes[it.key]?.cancel()
it.value.close() it.value.closeVolume()
} }
volumes.clear() volumes.clear()
volumesData.clear() volumesData.clear()
observers.forEach { it.onAllVolumesClosed() }
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootsChanged(context)
} }
} }

View File

@ -1,40 +1,88 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.util.AndroidUtils
class VolumeManagerApp : Application(), DefaultLifecycleObserver { class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object { 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 closingServiceIntent by lazy {
private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> Intent(this, ClosingService::class.java)
if (key == USF_KEEP_OPEN_KEY) {
reloadUsfKeepOpen()
} }
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 var usfKeepOpen = false 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 isExporting = false
var isStartingExternalApp = 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() { override fun onCreate() {
super<Application>.onCreate() super<Application>.onCreate()
ProcessLifecycleOwner.get().lifecycle.addObserver(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this).apply { AndroidUtils.LiveBooleanPreference.init(this, usfBackgroundDelegate, usfKeepOpenDelegate)
registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
}
reloadUsfKeepOpen()
} }
private fun reloadUsfKeepOpen() { fun updateServicesStates() {
usfKeepOpen = sharedPreferences.getBoolean(USF_KEEP_OPEN_KEY, false) 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) { override fun onResume(owner: LifecycleOwner) {
@ -43,10 +91,10 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
if (!isStartingExternalApp) { if (!isStartingExternalApp) {
if (!usfKeepOpen) { if (!usfBackground) {
volumeManager.closeAll() volumeManager.closeAll()
} }
if (!usfKeepOpen || !isExporting) { if (!usfBackground || !isExporting) {
TemporaryFileProvider.instance.wipe() TemporaryFileProvider.instance.wipe()
} }
} }

View File

@ -211,7 +211,7 @@ class VolumeOpener(
private var isClosed = false private var isClosed = false
override fun onFailed(pending: Boolean) { override fun onFailed(pending: Boolean) {
if (!isClosed) { if (!isClosed) {
encryptedVolume.close() encryptedVolume.closeVolume()
isClosed = true isClosed = true
} }
Arrays.fill(returnedHash.value!!, 0) Arrays.fill(returnedHash.value!!, 0)

View File

@ -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()
}
}

View File

@ -40,6 +40,12 @@ abstract class SelectableAdapter<T>(private val onSelectionChanged: (Int) -> Uni
return true return true
} }
fun unselect(position: Int) {
selectedItems.remove(position)
onSelectionChanged(selectedItems.size)
notifyItemChanged(position)
}
fun selectAll() { fun selectAll() {
for (i in getItems().indices) { for (i in getItems().indices) {
if (!selectedItems.contains(i) && isSelectable(i)) { if (!selectedItems.contains(i) && isSelectable(i)) {

View File

@ -29,6 +29,16 @@ class VolumeAdapter(
init { init {
reloadVolumes() reloadVolumes()
volumeManager.observe(object : VolumeManager.Observer {
override fun onVolumeStateChanged(volume: VolumeData) {
notifyItemChanged(volumes.indexOf(volume))
}
@SuppressLint("NotifyDataSetChanged")
override fun onAllVolumesClosed() {
notifyDataSetChanged()
}
})
} }
interface Listener { interface Listener {
@ -66,7 +76,7 @@ class VolumeAdapter(
false false
} }
fun onVolumeChanged(position: Int) { fun onVolumeDataChanged(position: Int) {
reloadVolumes() reloadVolumes()
notifyItemChanged(position) notifyItemChanged(position)
} }

View File

@ -10,7 +10,6 @@ import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.preference.PreferenceManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -18,6 +17,7 @@ import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.EncryptedFileProvider import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.VolumeManager import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.Wiper import sushi.hardcore.droidfs.util.Wiper
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -36,9 +36,10 @@ class TemporaryFileProvider : ContentProvider() {
lateinit var instance: TemporaryFileProvider lateinit var instance: TemporaryFileProvider
private set private set
var usfSafWrite = false
} }
private val usfSafWriteDelegate = AndroidUtils.LiveBooleanPreference("usf_saf_write", false)
private val usfSafWrite by usfSafWriteDelegate
private lateinit var volumeManager: VolumeManager private lateinit var volumeManager: VolumeManager
lateinit var encryptedFileProvider: EncryptedFileProvider lateinit var encryptedFileProvider: EncryptedFileProvider
private val files = HashMap<Uri, ProvidedFile>() private val files = HashMap<Uri, ProvidedFile>()
@ -46,8 +47,7 @@ class TemporaryFileProvider : ContentProvider() {
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
return context?.let { return context?.let {
volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager
usfSafWrite = usfSafWriteDelegate.init(it)
PreferenceManager.getDefaultSharedPreferences(it).getBoolean("usf_saf_write", false)
encryptedFileProvider = EncryptedFileProvider(it) encryptedFileProvider = EncryptedFileProvider(it)
instance = this instance = this
val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it) val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it)

View File

@ -18,6 +18,7 @@ import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.io.File import java.io.File
@ -40,23 +41,23 @@ class VolumeProvider: DocumentsProvider() {
DocumentsContract.Document.COLUMN_SIZE, DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_LAST_MODIFIED, DocumentsContract.Document.COLUMN_LAST_MODIFIED,
) )
var usfExpose = false
var usfSafWrite = false
fun notifyRootsChanged(context: Context) { fun notifyRootsChanged(context: Context) {
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null) 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 lateinit var volumeManager: VolumeManager
private val volumes = HashMap<String, Pair<Int, VolumeData>>() private val volumes = HashMap<String, Pair<Int, VolumeData>>()
private lateinit var encryptedFileProvider: EncryptedFileProvider private lateinit var encryptedFileProvider: EncryptedFileProvider
override fun onCreate(): Boolean { override fun onCreate(): Boolean {
val context = (context ?: return false) val context = (context ?: return false)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) AndroidUtils.LiveBooleanPreference.init(context, usfExposeDelegate, usfSafWriteDelegate)
usfExpose = sharedPreferences.getBoolean("usf_expose", false)
usfSafWrite = sharedPreferences.getBoolean("usf_saf_write", false)
volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager
encryptedFileProvider = EncryptedFileProvider(context) encryptedFileProvider = EncryptedFileProvider(context)
return true return true

View File

@ -1,12 +1,8 @@
package sushi.hardcore.droidfs.explorers package sushi.hardcore.droidfs.explorers
import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -15,7 +11,6 @@ import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -27,7 +22,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -55,6 +49,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.util.UIUtils
import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -100,7 +95,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
usf_open = sharedPrefs.getBoolean("usf_open", false) usf_open = sharedPrefs.getBoolean("usf_open", false)
volumeName = intent.getStringExtra("volumeName") ?: "" volumeName = intent.getStringExtra("volumeName") ?: ""
volumeId = intent.getIntExtra("volumeId", -1) 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) sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values) sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -199,7 +194,7 @@ 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 { val intent = Intent(this, cls).apply {
putExtra("path", filePath) putExtra("path", filePath)
putExtra("volume", encryptedVolume) putExtra("volumeId", volumeId)
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex]) putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
} }
startActivity(intent) startActivity(intent)
@ -679,10 +674,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
if (app.isStartingExternalApp) { if (app.isStartingExternalApp) {
TemporaryFileProvider.instance.wipe() TemporaryFileProvider.instance.wipe()
} }
if (encryptedVolume.isClosed()) {
finish()
} else {
setCurrentPath(currentDirectoryPath) setCurrentPath(currentDirectoryPath)
} }
}
} }

View File

@ -181,7 +181,6 @@ class ExplorerActivity : BaseExplorerActivity() {
"importFromOtherVolumes" -> { "importFromOtherVolumes" -> {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
intent.action = "pick" intent.action = "pick"
intent.putExtra("volume", encryptedVolume)
pickFromOtherVolumes.launch(intent) pickFromOtherVolumes.launch(intent)
} }
"importFiles" -> { "importFiles" -> {
@ -205,7 +204,7 @@ class ExplorerActivity : BaseExplorerActivity() {
"camera" -> { "camera" -> {
val intent = Intent(this, CameraActivity::class.java) val intent = Intent(this, CameraActivity::class.java)
intent.putExtra("path", currentDirectoryPath) intent.putExtra("path", currentDirectoryPath)
intent.putExtra("volume", encryptedVolume) intent.putExtra("volumeId", volumeId)
startActivity(intent) startActivity(intent)
} }
} }

View File

@ -1,7 +1,5 @@
package sushi.hardcore.droidfs.file_operations package sushi.hardcore.droidfs.file_operations
import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -11,7 +9,6 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.net.Uri import android.net.Uri
import android.os.Binder import android.os.Binder
@ -21,7 +18,6 @@ import android.os.IBinder
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
@ -39,12 +35,14 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.NotificationBroadcastReceiver
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManager import sushi.hardcore.droidfs.VolumeManager
import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.AndroidUtils
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper 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. * If multiple activities bind simultaneously, only the latest one will be used by the service.
*/ */
fun bind(activity: BaseActivity, onBound: (FileOperationService) -> Unit) { fun bind(activity: BaseActivity, onBound: (FileOperationService) -> Unit) {
var service: FileOperationService? = null val helper = AndroidUtils.NotificationPermissionHelper(activity)
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()
}
}
activity.bindService(Intent(activity, FileOperationService::class.java), object : ServiceConnection { activity.bindService(Intent(activity, FileOperationService::class.java), object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) { override fun onServiceConnected(className: ComponentName, binder: IBinder) {
onBound((binder as FileOperationService.LocalBinder).getService().also { onBound((binder as FileOperationService.LocalBinder).getService().also {
it.notificationPermissionLauncher = launcher it.notificationPermissionHelper = helper
service = it
}) })
} }
override fun onServiceDisconnected(arg0: ComponentName) {} override fun onServiceDisconnected(arg0: ComponentName) {}
@ -128,7 +105,7 @@ class FileOperationService : Service() {
private val binder = LocalBinder() private val binder = LocalBinder()
private lateinit var volumeManger: VolumeManager private lateinit var volumeManger: VolumeManager
private var serviceScope = MainScope() private var serviceScope = MainScope()
private lateinit var notificationPermissionLauncher: ActivityResultLauncher<String> private lateinit var notificationPermissionHelper: AndroidUtils.NotificationPermissionHelper<BaseActivity>
private var askForNotificationPermission = true private var askForNotificationPermission = true
private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationManager: NotificationManagerCompat
private val notifications = HashMap<Int, NotificationCompat.Builder>() private val notifications = HashMap<Int, NotificationCompat.Builder>()
@ -315,19 +292,31 @@ class FileOperationService : Service() {
continuation.resume(Pair(taskId, job)) continuation.resume(Pair(taskId, job))
} }
pendingTask = task pendingTask = task
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (askForNotificationPermission) {
if ( notificationPermissionHelper.askAndRun { granted ->
ContextCompat.checkSelfPermission( if (granted) {
this, processPendingTask()
Manifest.permission.POST_NOTIFICATIONS } else {
) != PackageManager.PERMISSION_GRANTED CustomAlertDialogBuilder(notificationPermissionHelper.activity, notificationPermissionHelper.activity.theme)
&& askForNotificationPermission .setTitle(R.string.warning)
) { .setMessage(R.string.notification_denied_msg)
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) .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 askForNotificationPermission = false // only ask once per service instance
return@suspendCoroutine return@suspendCoroutine
} }
}
processPendingTask() processPendingTask()
} }
return waitForTask(startedTask.first, startedTask.second, onCancelled) return waitForTask(startedTask.first, startedTask.second, onCancelled)

View File

@ -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"))
}
}
}
}

View File

@ -18,10 +18,12 @@ import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.FileTypes import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
abstract class FileViewerActivity: BaseActivity() { abstract class FileViewerActivity: BaseActivity() {
@ -40,7 +42,10 @@ abstract class FileViewerActivity: BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!! filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath) 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) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask -> windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask ->
@ -173,11 +178,4 @@ abstract class FileViewerActivity: BaseActivity() {
protected fun goBackToExplorer() { protected fun goBackToExplorer() {
finish() finish()
} }
override fun onResume() {
super.onResume()
if (encryptedVolume.isClosed()) {
finish()
}
}
} }

View File

@ -1,6 +1,5 @@
package sushi.hardcore.droidfs.filesystems package sushi.hardcore.droidfs.filesystems
import android.os.Parcel
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement 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 { override fun openFileReadMode(path: String): Long {
return nativeOpen(fusePtr, path, 0) return nativeOpen(fusePtr, path, 0)
} }

View File

@ -2,18 +2,21 @@ package sushi.hardcore.droidfs.filesystems
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.VolumeData import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.Observable
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
abstract class EncryptedVolume: Parcelable { abstract class EncryptedVolume: Observable<EncryptedVolume.Observer>() {
interface Observer {
fun onClose()
}
class InitResult( class InitResult(
val errorCode: Int, val errorCode: Int,
@ -35,18 +38,6 @@ abstract class EncryptedVolume: Parcelable {
const val GOCRYPTFS_VOLUME_TYPE: Byte = 0 const val GOCRYPTFS_VOLUME_TYPE: Byte = 0
const val CRYFS_VOLUME_TYPE: Byte = 1 const val CRYFS_VOLUME_TYPE: Byte = 1
@JvmField
val CREATOR = object : Parcelable.Creator<EncryptedVolume> {
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<EncryptedVolume>(size)
}
/** /**
* Get the type of a volume. * 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 openFileReadMode(path: String): Long
abstract fun openFileWriteMode(path: String): Long abstract fun openFileWriteMode(path: String): Long
abstract fun read(fileHandle: Long, fileOffset: Long, buffer: ByteArray, dstOffset: Long, length: Long): Int 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 rmdir(path: String): Boolean
abstract fun getAttr(path: String): Stat? abstract fun getAttr(path: String): Stat?
abstract fun rename(srcPath: String, dstPath: String): Boolean abstract fun rename(srcPath: String, dstPath: String): Boolean
abstract fun close() protected abstract fun close()
abstract fun isClosed(): Boolean abstract fun isClosed(): Boolean
fun closeVolume() {
observers.forEach { it.onClose() }
close()
}
fun pathExists(path: String): Boolean { fun pathExists(path: String): Boolean {
return getAttr(path) != null return getAttr(path) != null
} }

View File

@ -1,6 +1,5 @@
package sushi.hardcore.droidfs.filesystems package sushi.hardcore.droidfs.filesystems
import android.os.Parcel
import android.util.Log import android.util.Log
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement 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 { override fun openFileReadMode(path: String): Long {
return native_open_read_mode(sessionID, path).toLong() 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) return native_get_attr(sessionID, path)
} }
override fun writeToParcel(parcel: Parcel, flags: Int) = with(parcel) {
writeByte(GOCRYPTFS_VOLUME_TYPE)
writeInt(sessionID)
}
override fun close() { override fun close() {
native_close(sessionID) native_close(sessionID)
} }

View File

@ -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<A: AppCompatActivity>(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)
}
}
}
}
}

View File

@ -0,0 +1,21 @@
package sushi.hardcore.droidfs.util
import android.app.Activity
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
abstract class Observable<T> {
protected val observers = mutableListOf<T>()
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
}
})
}

View File

@ -73,7 +73,6 @@
<string name="usf_screenshot">السماح بلقطة شاشة</string> <string name="usf_screenshot">السماح بلقطة شاشة</string>
<string name="usf_fingerprint">السماح بحفظ تجزئة كلمة المرور باستخدام بصمة الإصبع</string> <string name="usf_fingerprint">السماح بحفظ تجزئة كلمة المرور باستخدام بصمة الإصبع</string>
<string name="usf_volume_management">إدارة مجلد التشفير</string> <string name="usf_volume_management">إدارة مجلد التشفير</string>
<string name="usf_keep_open">إبقاء مجلد التشفير مفتوحاً عند الخروج من التطبيق</string>
<string name="unsafe_features">الميزات غير الآمنة</string> <string name="unsafe_features">الميزات غير الآمنة</string>
<string name="manage_unsafe_features">إدارة الميزات غير الآمنة</string> <string name="manage_unsafe_features">إدارة الميزات غير الآمنة</string>
<string name="manage_unsafe_features_summary">تمكين / تعطيل الميزات غير الآمنة</string> <string name="manage_unsafe_features_summary">تمكين / تعطيل الميزات غير الآمنة</string>

View File

@ -74,7 +74,6 @@
<string name="usf_screenshot">Screenshots zulassen</string> <string name="usf_screenshot">Screenshots zulassen</string>
<string name="usf_fingerprint">Kennwort-Hash mit Fingerabdruck speichern können</string> <string name="usf_fingerprint">Kennwort-Hash mit Fingerabdruck speichern können</string>
<string name="usf_volume_management">Volumenverwaltung</string> <string name="usf_volume_management">Volumenverwaltung</string>
<string name="usf_keep_open">Volumen offen halten, wenn die App in den Hintergrund geht</string>
<string name="unsafe_features">Unsichere Funktionen</string> <string name="unsafe_features">Unsichere Funktionen</string>
<string name="manage_unsafe_features">Sichere Funktionen verwalten</string> <string name="manage_unsafe_features">Sichere Funktionen verwalten</string>
<string name="manage_unsafe_features_summary">Aktivieren/Deaktivieren unsicherer Funktionen</string> <string name="manage_unsafe_features_summary">Aktivieren/Deaktivieren unsicherer Funktionen</string>

View File

@ -74,7 +74,6 @@
<string name="usf_screenshot">Permitir capturas de pantalla</string> <string name="usf_screenshot">Permitir capturas de pantalla</string>
<string name="usf_fingerprint">Permitir guardar el hash de la contraseña mediante la huella dactilar</string> <string name="usf_fingerprint">Permitir guardar el hash de la contraseña mediante la huella dactilar</string>
<string name="usf_volume_management">Gestión del volumen</string> <string name="usf_volume_management">Gestión del volumen</string>
<string name="usf_keep_open">Mantener el volumen abierto cuando la aplicación está en segundo plano</string>
<string name="unsafe_features">Características inseguras</string> <string name="unsafe_features">Características inseguras</string>
<string name="manage_unsafe_features">Gestionar las características inseguras</string> <string name="manage_unsafe_features">Gestionar las características inseguras</string>
<string name="manage_unsafe_features_summary">Activar/desactivar funciones inseguras</string> <string name="manage_unsafe_features_summary">Activar/desactivar funciones inseguras</string>

View File

@ -73,7 +73,6 @@
<string name="usf_screenshot">Permitir capturas da tela</string> <string name="usf_screenshot">Permitir capturas da tela</string>
<string name="usf_fingerprint">Permitir salvar o hash da senha usando impressão digital</string> <string name="usf_fingerprint">Permitir salvar o hash da senha usando impressão digital</string>
<string name="usf_volume_management">Gerenciador de volumes</string> <string name="usf_volume_management">Gerenciador de volumes</string>
<string name="usf_keep_open">Mantenha o volume aberto quando o app ficar em segundo plano</string>
<string name="unsafe_features">Opções perigosas</string> <string name="unsafe_features">Opções perigosas</string>
<string name="manage_unsafe_features">Gerenciar opções perigosas</string> <string name="manage_unsafe_features">Gerenciar opções perigosas</string>
<string name="manage_unsafe_features_summary">Alternar opções perigosas</string> <string name="manage_unsafe_features_summary">Alternar opções perigosas</string>

View File

@ -71,7 +71,6 @@
<string name="usf_screenshot">Разрешить снимки экрана</string> <string name="usf_screenshot">Разрешить снимки экрана</string>
<string name="usf_fingerprint">Разрешить сохранение хеша пароля отпечатком пальца</string> <string name="usf_fingerprint">Разрешить сохранение хеша пароля отпечатком пальца</string>
<string name="usf_volume_management">Управление томом</string> <string name="usf_volume_management">Управление томом</string>
<string name="usf_keep_open">Оставлять том открытым, когда DroidFS в фоне</string>
<string name="unsafe_features">Небезопасные функции</string> <string name="unsafe_features">Небезопасные функции</string>
<string name="manage_unsafe_features">Управление небезопасными функциями</string> <string name="manage_unsafe_features">Управление небезопасными функциями</string>
<string name="manage_unsafe_features_summary">Включить/отключить небезопасные функции</string> <string name="manage_unsafe_features_summary">Включить/отключить небезопасные функции</string>

View File

@ -74,7 +74,6 @@
<string name="usf_screenshot">Ekran görüntüsü almaya izin ver</string> <string name="usf_screenshot">Ekran görüntüsü almaya izin ver</string>
<string name="usf_fingerprint">Parmak izi kullanılarak şifre hash değerinin kaydedilmesine izin ver</string> <string name="usf_fingerprint">Parmak izi kullanılarak şifre hash değerinin kaydedilmesine izin ver</string>
<string name="usf_volume_management">Birim yönetimi</string> <string name="usf_volume_management">Birim yönetimi</string>
<string name="usf_keep_open">Uygulama arka plana geçtiğinde birimi açık tutun</string>
<string name="unsafe_features">Güvenli olmayan özellikler</string> <string name="unsafe_features">Güvenli olmayan özellikler</string>
<string name="manage_unsafe_features">Güvenli olmayan özellikleri yönetin</string> <string name="manage_unsafe_features">Güvenli olmayan özellikleri yönetin</string>
<string name="manage_unsafe_features_summary">Güvenli olmayan özellikleri etkinleştirme/devre dışı bırakma</string> <string name="manage_unsafe_features_summary">Güvenli olmayan özellikleri etkinleştirme/devre dışı bırakma</string>

View File

@ -75,7 +75,6 @@
<string name="usf_screenshot">允许截屏</string> <string name="usf_screenshot">允许截屏</string>
<string name="usf_fingerprint">允许通过指纹保存密码哈希</string> <string name="usf_fingerprint">允许通过指纹保存密码哈希</string>
<string name="usf_volume_management">加密卷管理</string> <string name="usf_volume_management">加密卷管理</string>
<string name="usf_keep_open">当应用切入后台时已打开的卷不再自动锁上</string>
<string name="unsafe_features">以下功能会降低安全性</string> <string name="unsafe_features">以下功能会降低安全性</string>
<string name="manage_unsafe_features">管理非安全功能</string> <string name="manage_unsafe_features">管理非安全功能</string>
<string name="manage_unsafe_features_summary">打开/关闭非安全功能</string> <string name="manage_unsafe_features_summary">打开/关闭非安全功能</string>

View File

@ -74,7 +74,6 @@
<string name="usf_screenshot">Allow screenshots</string> <string name="usf_screenshot">Allow screenshots</string>
<string name="usf_fingerprint">Allow saving password hash using fingerprint</string> <string name="usf_fingerprint">Allow saving password hash using fingerprint</string>
<string name="usf_volume_management">Volume Management</string> <string name="usf_volume_management">Volume Management</string>
<string name="usf_keep_open">Keep volume open when the app goes in background</string>
<string name="unsafe_features">Unsafe Features</string> <string name="unsafe_features">Unsafe Features</string>
<string name="manage_unsafe_features">Manage unsafe features</string> <string name="manage_unsafe_features">Manage unsafe features</string>
<string name="manage_unsafe_features_summary">Enable/Disable unsafe features</string> <string name="manage_unsafe_features_summary">Enable/Disable unsafe features</string>
@ -281,4 +280,11 @@
<string name="logcat_saved">Logcat saved</string> <string name="logcat_saved">Logcat saved</string>
<string name="later">Later</string> <string name="later">Later</string>
<string name="notification_denied_msg">Notification permission has been denied. Background file operations won\'t be visible. You can change this in the app\'s permission settings.</string> <string name="notification_denied_msg">Notification permission has been denied. Background file operations won\'t be visible. You can change this in the app\'s permission settings.</string>
<string name="keep_alive_notification_title">Keep alive service</string>
<string name="keep_alive_notification_text">One or more volumes are kept open.</string>
<string name="close_all">Close all</string>
<string name="usf_background">Disable volume auto-locking</string>
<string name="usf_background_summary">Don\'t lock volumes when the app goes in background</string>
<string name="usf_keep_open">Keep volumes open</string>
<string name="usf_keep_open_summary">Maintain the app always running in the background to keep volumes open</string>
</resources> </resources>

View File

@ -48,11 +48,19 @@
android:key="usf_fingerprint" android:key="usf_fingerprint"
android:title="@string/usf_fingerprint" /> android:title="@string/usf_fingerprint" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_lock_open"
android:key="usf_background"
android:title="@string/usf_background"
android:summary="@string/usf_background_summary" />
<SwitchPreference <SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:icon="@drawable/icon_lock_open" android:icon="@drawable/icon_lock_open"
android:key="usf_keep_open" android:key="usf_keep_open"
android:title="@string/usf_keep_open" /> android:title="@string/usf_keep_open"
android:summary="@string/usf_keep_open_summary"/>
</PreferenceCategory> </PreferenceCategory>