Skip to content

Commit

Permalink
Add support for hiding files on a remote when the app is locked
Browse files Browse the repository at this point in the history
This commit adds a new per-remote option for hiding files while the app
is locked. This is implemented as an extension of the existing allow
external access toggle. However, with this option:

* The SAF roots remain visible
* File operations throw FileNotFoundException instead of
  SecurityException

The idea is that external apps are temporarily blocked from access, but
still retain all previously granted permissions.

Unlike the original proposed design, queryChildDocuments() will also
throw FileNotFoundException instead of returning an empty list. This is
so the same function is used for enforce all security restrictions and
make it easy to verify the correctness of the logic.

When the inactivity period expires, the restrictions are enforced for
new file operations only. Ongoing operations, like file copies/moves,
and reads/writes against already-open file descriptors are still
allowed.

Fixes: #106

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
  • Loading branch information
chenxiaolong committed Dec 17, 2024
1 parent 4fe65d5 commit 0962e57
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 15 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/com/chiller3/rsaf/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Preferences(private val context: Context) {
const val PREF_DUPLICATE_REMOTE = "duplicate_remote"
const val PREF_DELETE_REMOTE = "delete_remote"
const val PREF_ALLOW_EXTERNAL_ACCESS = "allow_external_access"
const val PREF_ALLOW_LOCKED_ACCESS = "allow_locked_access"
const val PREF_DYNAMIC_SHORTCUT = "dynamic_shortcut"
const val PREF_VFS_CACHING = "vfs_caching"
const val PREF_REPORT_USAGE = "report_usage"
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/java/com/chiller3/rsaf/rclone/RcloneProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.system.Os
import android.system.OsConstants
import android.util.Log
import android.webkit.MimeTypeMap
import com.chiller3.rsaf.AppLock
import com.chiller3.rsaf.BuildConfig
import com.chiller3.rsaf.Notifications
import com.chiller3.rsaf.Permissions
Expand All @@ -36,6 +37,7 @@ import com.chiller3.rsaf.binding.rcbridge.RbFile
import com.chiller3.rsaf.binding.rcbridge.Rcbridge
import com.chiller3.rsaf.extension.toException
import com.chiller3.rsaf.extension.toSingleLineString
import java.io.FileNotFoundException
import java.io.IOException

class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreferenceChangeListener {
Expand Down Expand Up @@ -403,8 +405,13 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference
val config = configs[remote]
?: throw IllegalArgumentException("Remote does not exist: $remote")

if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_BLOCKED)) {
throw SecurityException("Access to remote is blocked: $remote")
if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_HARD_BLOCKED)) {
throw SecurityException("Access to remote is hard blocked: $remote")
}

val softBlocked = RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_SOFT_BLOCKED)
if (softBlocked && AppLock.isLocked) {
throw FileNotFoundException("Remote inaccessible while app is locked: $remote")
}
}
}
Expand All @@ -419,7 +426,7 @@ class RcloneProvider : DocumentsProvider(), SharedPreferences.OnSharedPreference
}

for ((remote, config) in RcloneRpc.remotes) {
if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_BLOCKED)) {
if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_HARD_BLOCKED)) {
debugLog("Skipping blocked remote: $remote")
continue
}
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/java/com/chiller3/rsaf/rclone/RcloneRpc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ object RcloneRpc {

private const val CUSTOM_OPT_PREFIX = "rsaf:"
// This is called hidden due to backwards compatibility.
const val CUSTOM_OPT_BLOCKED = CUSTOM_OPT_PREFIX + "hidden"
const val CUSTOM_OPT_HARD_BLOCKED = CUSTOM_OPT_PREFIX + "hidden"
const val CUSTOM_OPT_SOFT_BLOCKED = CUSTOM_OPT_PREFIX + "soft_blocked"
const val CUSTOM_OPT_DYNAMIC_SHORTCUT = CUSTOM_OPT_PREFIX + "dynamic_shortcut"
const val CUSTOM_OPT_VFS_CACHING = CUSTOM_OPT_PREFIX + "vfs_caching"
const val CUSTOM_OPT_REPORT_USAGE = CUSTOM_OPT_PREFIX + "report_usage"

private const val DEFAULT_BLOCKED = false
private const val DEFAULT_HARD_BLOCKED = false
private const val DEFAULT_SOFT_BLOCKED = false
private const val DEFAULT_DYNAMIC_SHORTCUT = false
private const val DEFAULT_VFS_CACHING = true
private const val DEFAULT_REPORT_USAGE = false
Expand Down Expand Up @@ -397,7 +399,8 @@ object RcloneRpc {
/** Get the custom option boolean value or return the default if unset or invalid. */
fun getCustomBoolOpt(config: Map<String, String>, opt: String): Boolean {
val default = when (opt) {
CUSTOM_OPT_BLOCKED -> DEFAULT_BLOCKED
CUSTOM_OPT_HARD_BLOCKED -> DEFAULT_HARD_BLOCKED
CUSTOM_OPT_SOFT_BLOCKED -> DEFAULT_SOFT_BLOCKED
CUSTOM_OPT_DYNAMIC_SHORTCUT -> DEFAULT_DYNAMIC_SHORTCUT
CUSTOM_OPT_VFS_CACHING -> DEFAULT_VFS_CACHING
CUSTOM_OPT_REPORT_USAGE -> DEFAULT_REPORT_USAGE
Expand Down
27 changes: 20 additions & 7 deletions app/src/main/java/com/chiller3/rsaf/settings/EditRemoteFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
private lateinit var prefDuplicateRemote: Preference
private lateinit var prefDeleteRemote: Preference
private lateinit var prefAllowExternalAccess: SwitchPreferenceCompat
private lateinit var prefAllowLockedAccess: SwitchPreferenceCompat
private lateinit var prefDynamicShortcut: SwitchPreferenceCompat
private lateinit var prefVfsCaching: SwitchPreferenceCompat
private lateinit var prefReportUsage: SwitchPreferenceCompat
Expand Down Expand Up @@ -84,6 +85,9 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
prefAllowExternalAccess = findPreference(Preferences.PREF_ALLOW_EXTERNAL_ACCESS)!!
prefAllowExternalAccess.onPreferenceChangeListener = this

prefAllowLockedAccess = findPreference(Preferences.PREF_ALLOW_LOCKED_ACCESS)!!
prefAllowLockedAccess.onPreferenceChangeListener = this

prefDynamicShortcut = findPreference(Preferences.PREF_DYNAMIC_SHORTCUT)!!
prefDynamicShortcut.onPreferenceChangeListener = this

Expand All @@ -108,20 +112,26 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.remoteConfig.collect {
prefOpenRemote.isEnabled = it.allowExternalAccess ?: false
prefOpenRemote.isEnabled = it.allowExternalAccess == true

prefAllowExternalAccess.isEnabled = it.allowExternalAccess != null
if (it.allowExternalAccess != null) {
prefAllowExternalAccess.isChecked = it.allowExternalAccess
}

prefDynamicShortcut.isEnabled = it.allowExternalAccess ?: false
prefAllowLockedAccess.isEnabled = it.allowExternalAccess == true
&& prefs.requireAuth
if (it.allowLockedAccess != null) {
prefAllowLockedAccess.isChecked = it.allowLockedAccess
}

prefDynamicShortcut.isEnabled = it.allowExternalAccess == true
if (it.dynamicShortcut != null) {
prefDynamicShortcut.isChecked = it.dynamicShortcut
}

prefVfsCaching.isEnabled = it.allowExternalAccess ?: false
&& it.features?.putStream ?: false
prefVfsCaching.isEnabled = it.allowExternalAccess == true
&& it.features?.putStream == true
if (it.vfsCaching != null) {
prefVfsCaching.isChecked = it.vfsCaching
}
Expand All @@ -131,8 +141,8 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
false -> getString(R.string.pref_edit_remote_vfs_caching_desc_required)
}

prefReportUsage.isEnabled = it.allowExternalAccess ?: false
&& it.features?.about ?: false
prefReportUsage.isEnabled = it.allowExternalAccess == true
&& it.features?.about == true
if (it.reportUsage != null) {
prefReportUsage.isChecked = it.reportUsage
}
Expand Down Expand Up @@ -257,6 +267,9 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
prefAllowExternalAccess -> {
viewModel.setExternalAccess(remote, newValue as Boolean)
}
prefAllowLockedAccess -> {
viewModel.setLockedAccess(remote, newValue as Boolean)
}
prefDynamicShortcut -> {
viewModel.setDynamicShortcut(remote, newValue as Boolean)
}
Expand Down Expand Up @@ -307,7 +320,7 @@ class EditRemoteFragment : PreferenceBaseFragment(), FragmentResultListener,
var rank = 0

for ((remote, config) in remotes) {
if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_BLOCKED)
if (RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_HARD_BLOCKED)
|| !RcloneRpc.getCustomBoolOpt(config, RcloneRpc.CUSTOM_OPT_DYNAMIC_SHORTCUT)) {
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ data class EditRemoteActivityActions(

data class RemoteConfigState(
val allowExternalAccess: Boolean? = null,
val allowLockedAccess: Boolean? = null,
val dynamicShortcut: Boolean? = null,
val vfsCaching: Boolean? = null,
val reportUsage: Boolean? = null,
Expand Down Expand Up @@ -72,7 +73,11 @@ class EditRemoteViewModel : ViewModel() {
it.copy(
allowExternalAccess = !RcloneRpc.getCustomBoolOpt(
config,
RcloneRpc.CUSTOM_OPT_BLOCKED,
RcloneRpc.CUSTOM_OPT_HARD_BLOCKED,
),
allowLockedAccess = !RcloneRpc.getCustomBoolOpt(
config,
RcloneRpc.CUSTOM_OPT_SOFT_BLOCKED,
),
dynamicShortcut = RcloneRpc.getCustomBoolOpt(
config,
Expand Down Expand Up @@ -135,11 +140,15 @@ class EditRemoteViewModel : ViewModel() {
}

fun setExternalAccess(remote: String, allow: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_BLOCKED, !allow) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_HARD_BLOCKED, !allow) {
_activityActions.update { it.copy(refreshRoots = true) }
}
}

fun setLockedAccess(remote: String, allow: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_SOFT_BLOCKED, !allow)
}

fun setDynamicShortcut(remote: String, enabled: Boolean) {
setCustomOpt(remote, RcloneRpc.CUSTOM_OPT_DYNAMIC_SHORTCUT, enabled)
}
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
<string name="pref_edit_remote_delete_desc">Remove this remote from the configuration.</string>
<string name="pref_edit_remote_allow_external_access_name">Allow external app access</string>
<string name="pref_edit_remote_allow_external_access_desc">Allow external apps to access this remote via the system file manager. Access is not needed if this remote is just a backend for another remote.</string>
<string name="pref_edit_remote_allow_locked_access_name">Allow access while locked</string>
<string name="pref_edit_remote_allow_locked_access_desc_on">While RSAF is locked, files are still available to external apps that have been granted access.</string>
<string name="pref_edit_remote_allow_locked_access_desc_off">While RSAF is locked, files are hidden from external apps and new file operations are blocked. Ongoing operations with files that are already open are unaffected.</string>
<string name="pref_edit_remote_dynamic_shortcut_name">Show in launcher shortcuts</string>
<string name="pref_edit_remote_dynamic_shortcut_desc">Include this remote in the list of shortcuts when long pressing RSAF\'s launcher icon.</string>
<string name="pref_edit_remote_vfs_caching_name">Enable VFS caching</string>
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/preferences_edit_remote.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@
app:iconSpaceReserved="false"
app:defaultValue="true" />

<SwitchPreferenceCompat
app:key="allow_locked_access"
app:persistent="false"
app:title="@string/pref_edit_remote_allow_locked_access_name"
app:summaryOn="@string/pref_edit_remote_allow_locked_access_desc_on"
app:summaryOff="@string/pref_edit_remote_allow_locked_access_desc_off"
app:iconSpaceReserved="false"
app:defaultValue="true" />

<SwitchPreferenceCompat
app:key="dynamic_shortcut"
app:persistent="false"
Expand Down

0 comments on commit 0962e57

Please sign in to comment.