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