mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Library sharing: make the recent-games provider opt-in
RecentGamesContentProvider (PR #566) ships android:exported="true" with no android:permission, no readPermission, and no caller check in query(). That is required for the feature to work -- a signature-level permission would only admit apps we sign, and the companion app it exists for is third-party -- but as merged it means every app on the device, holding no permissions at all, can read the recently-played list: titles, serials, last-played times, and the file URIs, which carry the user's folder layout and frequently their real name. No prompt, no way to turn it off. Gate query() on a preference that defaults to OFF, and surface it in App settings, matching how the second-screen panel and Discord presence are handled: anything that exposes data outside the app is the user's decision and starts disabled. The flag lives in the same "ARMSX2" SharedPreferences file the provider already reads for the library itself, so the toggle and the gate are one value rather than two that can drift. Written with commit() and not apply(): the reader is a different process and can be queried the moment the switch returns, and apply() only promises the in-memory value. Returns an empty cursor rather than null when sharing is off -- null is the failure signal a ContentResolver caller has to special-case, and "the user has not enabled this" is a legitimate answer rather than an error.
This commit is contained in:
+21
@@ -33,6 +33,11 @@ class RecentGamesContentProvider : ContentProvider() {
|
||||
private const val LAST_PLAYED_PREFIX = "playtime.last."
|
||||
private const val MAX_RECENT_LOOKUP = 12
|
||||
|
||||
/** Master switch for this provider, default OFF — see the gate in [query].
|
||||
* Lives in the same "ARMSX2" prefs file the app writes, so the App-settings
|
||||
* toggle and this provider are reading one value and not two. */
|
||||
const val KEY_SHARE_ENABLED = "library.shareRecentGames"
|
||||
|
||||
const val PATH_GAMES = "games"
|
||||
const val COLUMN_URI = "uri"
|
||||
const val COLUMN_TITLE = "title"
|
||||
@@ -58,6 +63,22 @@ class RecentGamesContentProvider : ContentProvider() {
|
||||
val context = context ?: return cursor
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// ★ OPT-IN, and the gate has to be HERE rather than in the manifest.
|
||||
//
|
||||
// The provider is android:exported="true" with no permission, which it must be for a
|
||||
// third-party companion app to reach it at all — a signature-level permission would only
|
||||
// admit apps we sign. Exported and ungated means every app on the device, holding no
|
||||
// permissions of any kind, can read the library: titles, serials, last-played times, and
|
||||
// the file URIs, which carry the user's folder layout and often their real name.
|
||||
//
|
||||
// So sharing is the user's decision and it starts off, matching how the second-screen
|
||||
// panel and Discord presence are handled. An empty cursor rather than null: null is the
|
||||
// failure signal a ContentResolver caller has to special-case, and "sharing is off" is a
|
||||
// legitimate answer, not an error.
|
||||
if (!prefs.getBoolean(KEY_SHARE_ENABLED, false)) {
|
||||
return cursor
|
||||
}
|
||||
|
||||
val recentUris = readRecentUris(prefs)
|
||||
if (recentUris.isEmpty()) {
|
||||
return cursor
|
||||
|
||||
@@ -1337,6 +1337,8 @@ private val BASE_EN: Map<String, String> = mapOf(
|
||||
"renderer.upscale.custom" to "Custom",
|
||||
"renderer.upscale.customScale" to "Custom resolution scale",
|
||||
"renderer.upscale.customScale.description" to "Set the internal resolution as a percentage of native, for steps the presets don't cover. For example 107% renders at roughly true 480p height, sharper than native without the cost of 2x. Higher values look better but are heavier on the GPU.",
|
||||
"app.shareRecentGames" to "Share recently played with other apps",
|
||||
"app.shareRecentGames.desc" to "Lets companion apps on this device read your recently-played list — game titles, serials, last-played times and file paths. Off by default: any app can read it while this is on, without asking. Turn it on only if you use a companion app that needs it.",
|
||||
"secondScreen.label" to "Second screen panel",
|
||||
"secondScreen.desc" to "On devices with a second display (Ayn Thor, Retroid dual-screen and similar), show a panel there with live FPS, the battery and clock, and buttons for save state, load state, fast-forward, pause and screenshot. Does nothing when no second display is attached.",
|
||||
"secondScreen.noGame" to "No game running — start one to use save states, fast-forward and macros here.",
|
||||
|
||||
@@ -543,6 +543,32 @@ fun AppTab() {
|
||||
) { Text(str("secondScreen.layout.reset")) }
|
||||
}
|
||||
|
||||
// Companion-app access to the recently-played list, over the RecentGamesContentProvider.
|
||||
// Off by default and deliberately so: the provider is exported without a permission (it
|
||||
// has to be, for a third-party companion to reach it), so while this is on, any app on
|
||||
// the device can read the list — including the file URIs, which carry your folder layout.
|
||||
run {
|
||||
val shareKey = com.armsx2.data.library.RecentGamesContentProvider.KEY_SHARE_ENABLED
|
||||
val shareRecent = remember {
|
||||
mutableStateOf(
|
||||
com.armsx2.runtime.MainActivityRuntime.prefs.getBoolean(shareKey, false),
|
||||
)
|
||||
}
|
||||
ToggleRow(
|
||||
label = str("app.shareRecentGames"),
|
||||
value = shareRecent.value,
|
||||
description = str("app.shareRecentGames.desc"),
|
||||
) { on ->
|
||||
shareRecent.value = on
|
||||
// commit(), not apply(): the reader is a DIFFERENT process that can be queried
|
||||
// the moment this returns, and apply() only guarantees the in-memory value.
|
||||
runCatching {
|
||||
com.armsx2.runtime.MainActivityRuntime.prefs.edit()
|
||||
.putBoolean(shareKey, on).commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow(
|
||||
label = str("app.batteryWarnings"),
|
||||
value = com.armsx2.BatteryWatcher.enabled.value,
|
||||
|
||||
Reference in New Issue
Block a user