forked from hardcoresushi/DroidFS
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:
|
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**.
|
||||||
|
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
|
## 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
|
||||||
|
@ -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>
|
||||||
|
@ -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())
|
||||||
|
@ -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) {
|
||||||
|
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
|
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()) {
|
||||||
|
@ -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.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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
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)) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
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_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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user