KeepAlive foreground service
This commit is contained in:
parent
52a29b034c
commit
33d565bf22
2
BUILD.md
2
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**.
|
||||
|
3
TODO.md
3
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
|
||||
|
@ -58,10 +58,11 @@
|
||||
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
|
||||
<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"/>
|
||||
|
||||
<receiver android:name=".file_operations.NotificationBroadcastReceiver" android:exported="false">
|
||||
<receiver android:name=".NotificationBroadcastReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="file_operation_cancel"/>
|
||||
</intent-filter>
|
||||
|
@ -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())
|
||||
|
@ -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) {
|
||||
|
20
app/src/main/java/sushi/hardcore/droidfs/ClosingService.kt
Normal file
20
app/src/main/java/sushi/hardcore/droidfs/ClosingService.kt
Normal 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()
|
||||
}
|
||||
}
|
120
app/src/main/java/sushi/hardcore/droidfs/KeepAliveService.kt
Normal file
120
app/src/main/java/sushi/hardcore/droidfs/KeepAliveService.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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()) {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<SwitchPreference>("usf_background")!!
|
||||
val switchKeepOpen = findPreference<SwitchPreference>("usf_keep_open")!!
|
||||
val switchExternalOpen = findPreference<SwitchPreference>("usf_open")!!
|
||||
val switchExpose = findPreference<SwitchPreference>("usf_expose")!!
|
||||
val switchSafWrite = findPreference<SwitchPreference>("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<ListPreference>("export_method")!!.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue as String == "memory" && !Compat.isMemFileSupported()) {
|
||||
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
|
||||
|
@ -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<VolumeManager.Observer>() {
|
||||
interface Observer {
|
||||
fun onVolumeStateChanged(volume: VolumeData) {}
|
||||
fun onAllVolumesClosed() {}
|
||||
}
|
||||
|
||||
class VolumeManager(private val context: Context) {
|
||||
private var id = 0
|
||||
private val volumes = HashMap<Int, EncryptedVolume>()
|
||||
private val volumesData = HashMap<VolumeData, Int>()
|
||||
@ -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)
|
||||
}
|
||||
}
|
@ -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<Application>.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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -40,6 +40,12 @@ abstract class SelectableAdapter<T>(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)) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<Uri, ProvidedFile>()
|
||||
@ -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)
|
||||
|
@ -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<String, Pair<Int, VolumeData>>()
|
||||
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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<String>
|
||||
private lateinit var notificationPermissionHelper: AndroidUtils.NotificationPermissionHelper<BaseActivity>
|
||||
private var askForNotificationPermission = true
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private val notifications = HashMap<Int, NotificationCompat.Builder>()
|
||||
@ -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()
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<EncryptedVolume.Observer>() {
|
||||
|
||||
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<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.
|
||||
*
|
||||
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
111
app/src/main/java/sushi/hardcore/droidfs/util/AndroidUtils.kt
Normal file
111
app/src/main/java/sushi/hardcore/droidfs/util/AndroidUtils.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
app/src/main/java/sushi/hardcore/droidfs/util/Observable.kt
Normal file
21
app/src/main/java/sushi/hardcore/droidfs/util/Observable.kt
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
@ -73,7 +73,6 @@
|
||||
<string name="usf_screenshot">السماح بلقطة شاشة</string>
|
||||
<string name="usf_fingerprint">السماح بحفظ تجزئة كلمة المرور باستخدام بصمة الإصبع</string>
|
||||
<string name="usf_volume_management">إدارة مجلد التشفير</string>
|
||||
<string name="usf_keep_open">إبقاء مجلد التشفير مفتوحاً عند الخروج من التطبيق</string>
|
||||
<string name="unsafe_features">الميزات غير الآمنة</string>
|
||||
<string name="manage_unsafe_features">إدارة الميزات غير الآمنة</string>
|
||||
<string name="manage_unsafe_features_summary">تمكين / تعطيل الميزات غير الآمنة</string>
|
||||
|
@ -74,7 +74,6 @@
|
||||
<string name="usf_screenshot">Screenshots zulassen</string>
|
||||
<string name="usf_fingerprint">Kennwort-Hash mit Fingerabdruck speichern können</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="manage_unsafe_features">Sichere Funktionen verwalten</string>
|
||||
<string name="manage_unsafe_features_summary">Aktivieren/Deaktivieren unsicherer Funktionen</string>
|
||||
|
@ -74,7 +74,6 @@
|
||||
<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_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="manage_unsafe_features">Gestionar las características inseguras</string>
|
||||
<string name="manage_unsafe_features_summary">Activar/desactivar funciones inseguras</string>
|
||||
|
@ -73,7 +73,6 @@
|
||||
<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_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="manage_unsafe_features">Gerenciar opções perigosas</string>
|
||||
<string name="manage_unsafe_features_summary">Alternar opções perigosas</string>
|
||||
|
@ -71,7 +71,6 @@
|
||||
<string name="usf_screenshot">Разрешить снимки экрана</string>
|
||||
<string name="usf_fingerprint">Разрешить сохранение хеша пароля отпечатком пальца</string>
|
||||
<string name="usf_volume_management">Управление томом</string>
|
||||
<string name="usf_keep_open">Оставлять том открытым, когда DroidFS в фоне</string>
|
||||
<string name="unsafe_features">Небезопасные функции</string>
|
||||
<string name="manage_unsafe_features">Управление небезопасными функциями</string>
|
||||
<string name="manage_unsafe_features_summary">Включить/отключить небезопасные функции</string>
|
||||
|
@ -74,7 +74,6 @@
|
||||
<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_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="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>
|
||||
|
@ -75,7 +75,6 @@
|
||||
<string name="usf_screenshot">允许截屏</string>
|
||||
<string name="usf_fingerprint">允许通过指纹保存密码哈希</string>
|
||||
<string name="usf_volume_management">加密卷管理</string>
|
||||
<string name="usf_keep_open">当应用切入后台时已打开的卷不再自动锁上</string>
|
||||
<string name="unsafe_features">以下功能会降低安全性</string>
|
||||
<string name="manage_unsafe_features">管理非安全功能</string>
|
||||
<string name="manage_unsafe_features_summary">打开/关闭非安全功能</string>
|
||||
|
@ -74,7 +74,6 @@
|
||||
<string name="usf_screenshot">Allow screenshots</string>
|
||||
<string name="usf_fingerprint">Allow saving password hash using fingerprint</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="manage_unsafe_features">Manage 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="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="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>
|
||||
|
@ -48,11 +48,19 @@
|
||||
android:key="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
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_lock_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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user