From ea1ef16307d7a4f3acd1cd514d71dde6af8fc735 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 14 Apr 2026 21:38:01 +0200 Subject: [PATCH] fix(android): support session-only SAF permissions on OEM devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some OEM ROMs throw SecurityException when takePersistableUriPermission is called, even with FLAG_GRANT_PERSISTABLE_URI_PERMISSION requested. Instead of rejecting the call, fall through and return the URI — it remains valid for the session. Also fix checkUriPermission to use context.checkUriPermission() which detects both persistent grants and active session grants, so isReady() no longer clears a working (session-only) SAF URI on those devices. --- .../plugins/SafBridgePlugin.kt | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/com/superproductivity/superproductivity/plugins/SafBridgePlugin.kt b/android/app/src/main/java/com/superproductivity/superproductivity/plugins/SafBridgePlugin.kt index 060f3743d8..48b72b8fd2 100644 --- a/android/app/src/main/java/com/superproductivity/superproductivity/plugins/SafBridgePlugin.kt +++ b/android/app/src/main/java/com/superproductivity/superproductivity/plugins/SafBridgePlugin.kt @@ -2,7 +2,9 @@ package com.superproductivity.superproductivity.plugins import android.app.Activity import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Process import android.provider.DocumentsContract import androidx.activity.result.ActivityResult import androidx.documentfile.provider.DocumentFile @@ -40,18 +42,26 @@ class SafBridgePlugin : Plugin() { @ActivityCallback private fun handleFolderSelectionResult(call: PluginCall, result: ActivityResult) { if (result.resultCode == Activity.RESULT_OK) { - val uri = result.data?.data + val resultData = result.data + val uri = resultData?.data if (uri != null) { - // Take persistable permission + // Use only the flags that were actually granted by the result intent. + // Applying flags not present in the result throws SecurityException on some devices. + // resultData is Intent? but uri != null proves resultData is non-null here; + // Kotlin cannot infer this transitively, so !! is required and safe. + val grantedFlags = resultData!!.flags and + (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) try { - val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - context.contentResolver.takePersistableUriPermission(uri, takeFlags) + context.contentResolver.takePersistableUriPermission(uri, grantedFlags) } catch (e: SecurityException) { - // Some devices/Android versions fail to persist SAF permission - android.util.Log.w("SafBridgePlugin", "Failed to persist URI permission: ${e.message}") - call.reject("Failed to persist folder permission: ${e.message}") - return + // Some devices/OEM ROMs fail to persist SAF permission even when + // FLAG_GRANT_PERSISTABLE_URI_PERMISSION was requested. Fall through and + // still return the URI — it will work for this session even if it can't + // be persisted across reboots (user will need to re-select next launch). + android.util.Log.w( + "SafBridgePlugin", + "Could not persist URI permission (session-only access): ${e.message}", + ) } val ret = JSObject() @@ -208,8 +218,19 @@ class SafBridgePlugin : Plugin() { try { val uri = Uri.parse(uriString) - val persistedUris = context.contentResolver.persistedUriPermissions - val hasPermission = persistedUris.any { it.uri == uri && it.isReadPermission && it.isWritePermission } + // Check both persistent grants (survive reboots) and temporary session grants + // (active for current app session only). This is required to support devices/OEM + // ROMs where takePersistableUriPermission throws SecurityException — on those + // devices the session grant is still valid even though it could not be persisted. + val pid = Process.myPid() + val uid = Process.myUid() + val canRead = context.checkUriPermission( + uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION, + ) == PackageManager.PERMISSION_GRANTED + val canWrite = context.checkUriPermission( + uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + ) == PackageManager.PERMISSION_GRANTED + val hasPermission = canRead && canWrite val ret = JSObject() ret.put("hasPermission", hasPermission)