added gatekeeper notification options
This commit is contained in:
+9
-1
@@ -22,6 +22,7 @@ import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.PwaSessionCreator
|
||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentBlockNotifier
|
||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences
|
||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.GatekeeperNotificationActionReceiver
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -84,13 +85,20 @@ class IntentReceiverActivity : Activity() {
|
||||
private fun shouldBlockIntent(intent: Intent): Boolean {
|
||||
if (!IntentGatekeeperPreferences.isEnabled(applicationContext)) return false
|
||||
if (intent.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)) return false
|
||||
intent.getStringExtra(
|
||||
GatekeeperNotificationActionReceiver.EXTRA_NOTIFICATION_APPROVAL_TOKEN,
|
||||
)?.let { token ->
|
||||
if (IntentGatekeeperPreferences.hasNotificationApproval(applicationContext, token)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val caller = resolveCallerPackage(intent) ?: return false
|
||||
if (caller == packageName) return false
|
||||
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
||||
|
||||
Log.i(TAG, "Blocking intent from $caller (native gatekeeper)")
|
||||
IntentBlockNotifier.notifyBlocked(applicationContext, caller)
|
||||
IntentBlockNotifier.notifyBlocked(applicationContext, caller, intent)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*/
|
||||
package eu.weblibre.flutter_mozilla_components.gatekeeper
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/**
|
||||
* Handles notification action buttons on blocked-intent notifications.
|
||||
*
|
||||
* "Allow once" re-fires the original blocked intent to [IntentReceiverActivity]
|
||||
* with a one-shot approval marker so the already-approved launch bypasses both
|
||||
* the native and Flutter gatekeepers.
|
||||
*
|
||||
* "Always allow" additionally writes a pending decision to
|
||||
* [IntentGatekeeperPreferences] so that Flutter can persist the policy change
|
||||
* before the next gatekeeper decision, even if the app process is already up.
|
||||
*/
|
||||
class GatekeeperNotificationActionReceiver : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GatekeeperActionReceiver"
|
||||
|
||||
const val ACTION_ALLOW_ONCE = "eu.weblibre.gecko.gatekeeper.ALLOW_ONCE"
|
||||
const val ACTION_ALWAYS_ALLOW = "eu.weblibre.gecko.gatekeeper.ALWAYS_ALLOW"
|
||||
|
||||
const val EXTRA_PACKAGE_NAME = "gatekeeper_package"
|
||||
const val EXTRA_BLOCKED_INTENT = "gatekeeper_blocked_intent"
|
||||
const val EXTRA_NOTIFICATION_ID = "gatekeeper_notification_id"
|
||||
const val EXTRA_NOTIFICATION_APPROVAL_TOKEN = "eu.weblibre.gatekeeper.notification_approval_token"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: return
|
||||
val blockedIntent = getBlockedIntent(intent)
|
||||
cancelNotification(context, intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
|
||||
Log.d(TAG, "action=${intent.action} package=$packageName hasIntent=${blockedIntent != null}")
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_ALLOW_ONCE -> {
|
||||
blockedIntent?.let {
|
||||
openIntent(
|
||||
context = context,
|
||||
blockedIntent = it,
|
||||
approvalToken = IntentGatekeeperPreferences.createNotificationApproval(
|
||||
context,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ACTION_ALWAYS_ALLOW -> {
|
||||
IntentGatekeeperPreferences.addPendingAlwaysAllow(context, packageName)
|
||||
blockedIntent?.let {
|
||||
openIntent(
|
||||
context = context,
|
||||
blockedIntent = it,
|
||||
approvalToken = IntentGatekeeperPreferences.createNotificationApproval(
|
||||
context,
|
||||
alwaysAllowPackage = packageName,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelNotification(context: Context, notificationId: Int) {
|
||||
if (notificationId == -1) return
|
||||
|
||||
ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
?.cancel(notificationId)
|
||||
}
|
||||
|
||||
private fun getBlockedIntent(intent: Intent): Intent? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getParcelableExtra(EXTRA_BLOCKED_INTENT, Intent::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(EXTRA_BLOCKED_INTENT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openIntent(
|
||||
context: Context,
|
||||
blockedIntent: Intent,
|
||||
approvalToken: String,
|
||||
) {
|
||||
try {
|
||||
val relaunchIntent = Intent(blockedIntent).apply {
|
||||
setClassName(
|
||||
context.packageName,
|
||||
"eu.weblibre.flutter_mozilla_components.activities.IntentReceiverActivity",
|
||||
)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra(EXTRA_NOTIFICATION_APPROVAL_TOKEN, approvalToken)
|
||||
}
|
||||
context.startActivity(relaunchIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to replay blocked intent after gatekeeper action", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-12
@@ -6,10 +6,11 @@
|
||||
*/
|
||||
package eu.weblibre.flutter_mozilla_components.gatekeeper
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -17,22 +18,25 @@ import androidx.core.content.ContextCompat
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
|
||||
/**
|
||||
* Posts a purely informational notification when an intent is blocked by the
|
||||
* gatekeeper. The notification has no actions and no content intent.
|
||||
* Posts an actionable heads-up notification when an intent is blocked by the
|
||||
* native gatekeeper. The notification shows "Allow once" and "Always allow"
|
||||
* action buttons, and auto-dismisses after [TIMEOUT_MS] milliseconds.
|
||||
*/
|
||||
object IntentBlockNotifier {
|
||||
private const val CHANNEL_ID = "intent_gatekeeper_channel"
|
||||
private const val CHANNEL_ID = "intent_gatekeeper_channel_v2"
|
||||
private const val CHANNEL_NAME = "Blocked app launches"
|
||||
private const val CHANNEL_DESC = "Informs you when another app is prevented from opening WebLibre."
|
||||
private const val CHANNEL_DESC = "Shown when another app is prevented from opening WebLibre, with options to allow."
|
||||
|
||||
fun notifyBlocked(context: Context, packageName: String) {
|
||||
private const val TIMEOUT_MS = 8_000L
|
||||
|
||||
fun notifyBlocked(context: Context, packageName: String, blockedIntent: Intent) {
|
||||
val appCtx = context.applicationContext
|
||||
ensureChannel(appCtx)
|
||||
|
||||
val label = resolveAppLabel(appCtx, packageName) ?: packageName
|
||||
val notificationId = (System.currentTimeMillis() and 0x7FFFFFFF).toInt()
|
||||
|
||||
val notification: Notification = NotificationCompat.Builder(appCtx, CHANNEL_ID)
|
||||
val builder = NotificationCompat.Builder(appCtx, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle("Blocked app launch")
|
||||
.setContentText("Prevented $label from opening WebLibre.")
|
||||
@@ -40,15 +44,66 @@ object IntentBlockNotifier {
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText("Prevented $label from opening WebLibre.")
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setSilent(true)
|
||||
.setAutoCancel(true)
|
||||
.setShowWhen(true)
|
||||
.build()
|
||||
.setTimeoutAfter(TIMEOUT_MS)
|
||||
|
||||
builder.addAction(
|
||||
0,
|
||||
"Allow once",
|
||||
buildActionIntent(
|
||||
context = appCtx,
|
||||
action = GatekeeperNotificationActionReceiver.ACTION_ALLOW_ONCE,
|
||||
packageName = packageName,
|
||||
blockedIntent = blockedIntent,
|
||||
notificationId = notificationId,
|
||||
requestCode = notificationId + 1,
|
||||
),
|
||||
)
|
||||
|
||||
builder.addAction(
|
||||
0,
|
||||
"Always allow",
|
||||
buildActionIntent(
|
||||
context = appCtx,
|
||||
action = GatekeeperNotificationActionReceiver.ACTION_ALWAYS_ALLOW,
|
||||
packageName = packageName,
|
||||
blockedIntent = blockedIntent,
|
||||
notificationId = notificationId,
|
||||
requestCode = notificationId + 2,
|
||||
),
|
||||
)
|
||||
|
||||
val manager = ContextCompat.getSystemService(appCtx, NotificationManager::class.java)
|
||||
?: return
|
||||
manager.notify(notificationId, notification)
|
||||
manager.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
private fun buildActionIntent(
|
||||
context: Context,
|
||||
action: String,
|
||||
packageName: String,
|
||||
blockedIntent: Intent,
|
||||
notificationId: Int,
|
||||
requestCode: Int,
|
||||
): PendingIntent {
|
||||
val intent = Intent(action).apply {
|
||||
setClass(context, GatekeeperNotificationActionReceiver::class.java)
|
||||
putExtra(GatekeeperNotificationActionReceiver.EXTRA_PACKAGE_NAME, packageName)
|
||||
putExtra(GatekeeperNotificationActionReceiver.EXTRA_NOTIFICATION_ID, notificationId)
|
||||
putExtra(
|
||||
GatekeeperNotificationActionReceiver.EXTRA_BLOCKED_INTENT,
|
||||
Intent(blockedIntent),
|
||||
)
|
||||
}
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||
}
|
||||
|
||||
private fun ensureChannel(context: Context) {
|
||||
@@ -56,14 +111,16 @@ object IntentBlockNotifier {
|
||||
val manager = ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||
?: return
|
||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
||||
|
||||
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_DEFAULT,
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
).apply {
|
||||
description = CHANNEL_DESC
|
||||
setShowBadge(false)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
+80
@@ -8,6 +8,7 @@ package eu.weblibre.flutter_mozilla_components.gatekeeper
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Cross-package shared-prefs file used to replicate the Flutter-side intent
|
||||
@@ -21,6 +22,13 @@ object IntentGatekeeperPreferences {
|
||||
const val PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||
const val KEY_ENABLED = "enabled"
|
||||
const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
||||
const val KEY_PENDING_ALWAYS_ALLOW = "pending_always_allow"
|
||||
private const val KEY_NOTIFICATION_APPROVAL_TOKENS = "notification_approval_tokens"
|
||||
private const val KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX = "notification_approval_package_"
|
||||
|
||||
data class NotificationApproval(
|
||||
val alwaysAllowPackage: String?,
|
||||
)
|
||||
|
||||
fun get(context: Context): SharedPreferences =
|
||||
context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
@@ -34,4 +42,76 @@ object IntentGatekeeperPreferences {
|
||||
val blocked = prefs.getStringSet(KEY_BLOCKED_PACKAGES, emptySet()) ?: return false
|
||||
return packageName in blocked
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that the user tapped "Always allow" for [packageName] via a
|
||||
* notification action. Also removes [packageName] from the blocked set so
|
||||
* future native intents pass through immediately, without waiting for
|
||||
* Flutter to start and consume the pending decision.
|
||||
*/
|
||||
fun addPendingAlwaysAllow(context: Context, packageName: String) {
|
||||
val prefs = get(context)
|
||||
val pending = prefs.getStringSet(KEY_PENDING_ALWAYS_ALLOW, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
val blocked = prefs.getStringSet(KEY_BLOCKED_PACKAGES, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
pending.add(packageName)
|
||||
blocked.remove(packageName)
|
||||
prefs.edit()
|
||||
.putStringSet(KEY_PENDING_ALWAYS_ALLOW, pending)
|
||||
.putStringSet(KEY_BLOCKED_PACKAGES, blocked)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun createNotificationApproval(
|
||||
context: Context,
|
||||
alwaysAllowPackage: String? = null,
|
||||
): String {
|
||||
val prefs = get(context)
|
||||
val tokens =
|
||||
prefs.getStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, emptySet())
|
||||
?.toMutableSet()
|
||||
?: mutableSetOf()
|
||||
val token = UUID.randomUUID().toString()
|
||||
tokens.add(token)
|
||||
|
||||
prefs.edit()
|
||||
.putStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, tokens)
|
||||
.apply {
|
||||
if (alwaysAllowPackage != null) {
|
||||
putString(
|
||||
"$KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX$token",
|
||||
alwaysAllowPackage,
|
||||
)
|
||||
}
|
||||
}
|
||||
.apply()
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
fun hasNotificationApproval(context: Context, token: String): Boolean {
|
||||
val prefs = get(context)
|
||||
val tokens = prefs.getStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, emptySet()) ?: return false
|
||||
return token in tokens
|
||||
}
|
||||
|
||||
fun consumeNotificationApproval(context: Context, token: String): NotificationApproval? {
|
||||
val prefs = get(context)
|
||||
val tokens =
|
||||
prefs.getStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, emptySet())
|
||||
?.toMutableSet()
|
||||
?: return null
|
||||
if (!tokens.remove(token)) return null
|
||||
|
||||
val alwaysAllowPackage = prefs.getString(
|
||||
"$KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX$token",
|
||||
null,
|
||||
)
|
||||
|
||||
prefs.edit()
|
||||
.putStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, tokens)
|
||||
.remove("$KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX$token")
|
||||
.apply()
|
||||
|
||||
return NotificationApproval(alwaysAllowPackage = alwaysAllowPackage)
|
||||
}
|
||||
}
|
||||
|
||||
+21
@@ -24,6 +24,7 @@ class IntentGatekeeperHostApiImpl(private val context: Context) : IntentGatekeep
|
||||
private const val PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||
private const val KEY_ENABLED = "enabled"
|
||||
private const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
||||
private const val KEY_PENDING_ALWAYS_ALLOW = "pending_always_allow"
|
||||
}
|
||||
|
||||
override fun setConfig(enabled: Boolean, blockedPackages: List<String>) {
|
||||
@@ -50,4 +51,24 @@ class IntentGatekeeperHostApiImpl(private val context: Context) : IntentGatekeep
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPendingAlwaysAllows(): List<String> {
|
||||
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return (prefs.getStringSet(KEY_PENDING_ALWAYS_ALLOW, emptySet()) ?: emptySet()).toList()
|
||||
}
|
||||
|
||||
override fun ackPendingAlwaysAllows(packageNames: List<String>) {
|
||||
if (packageNames.isEmpty()) return
|
||||
|
||||
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val pending = prefs.getStringSet(KEY_PENDING_ALWAYS_ALLOW, emptySet())?.toMutableSet()
|
||||
?: mutableSetOf()
|
||||
if (pending.removeAll(packageNames.toSet())) {
|
||||
if (pending.isEmpty()) {
|
||||
prefs.edit().remove(KEY_PENDING_ALWAYS_ALLOW).apply()
|
||||
} else {
|
||||
prefs.edit().putStringSet(KEY_PENDING_ALWAYS_ALLOW, pending).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-4
@@ -36,6 +36,17 @@ import eu.weblibre.simple_intent_receiver.pigeons.Intent as PigeonIntent
|
||||
import eu.weblibre.simple_intent_receiver.pigeons.IntentGatekeeperHostApi
|
||||
|
||||
class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener {
|
||||
companion object {
|
||||
// Stable names that must match the notification replay path and shared-prefs schema.
|
||||
private const val PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||
private const val KEY_NOTIFICATION_APPROVAL_TOKENS = "notification_approval_tokens"
|
||||
private const val KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX = "notification_approval_package_"
|
||||
private const val EXTRA_NOTIFICATION_APPROVAL_TOKEN = "eu.weblibre.gatekeeper.notification_approval_token"
|
||||
private const val EXTRA_ALWAYS_ALLOW_PACKAGE = "eu.weblibre.gatekeeper.always_allow_package"
|
||||
}
|
||||
|
||||
private data class NotificationApproval(val alwaysAllowPackage: String?)
|
||||
|
||||
private lateinit var context: Context
|
||||
private var intentReceiver: IntentReceiver? = null
|
||||
private var lastHandledIntent: String? = null
|
||||
@@ -125,12 +136,17 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
}
|
||||
|
||||
val pigeonIntent = convertToPigeonIntent(intent)
|
||||
val notificationApproval = consumeNotificationApproval(intent)
|
||||
val pigeonIntent = convertToPigeonIntent(intent, notificationApproval)
|
||||
intentReceiver?.sendIntent(pigeonIntent)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveCallerPackage(intent: Intent): String? {
|
||||
private fun resolveCallerPackage(intent: Intent, notificationApproval: NotificationApproval?): String? {
|
||||
if (notificationApproval != null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val raw = resolveRawCallerPackage(intent) ?: return null
|
||||
// Treat system packages (launcher, shell, SystemUI, etc.) as internal — the
|
||||
// gatekeeper shouldn't prompt the user when the OS itself forwards an intent.
|
||||
@@ -179,10 +195,28 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToPigeonIntent(intent: Intent): PigeonIntent {
|
||||
private fun consumeNotificationApproval(intent: Intent): NotificationApproval? {
|
||||
val token = intent.getStringExtra(EXTRA_NOTIFICATION_APPROVAL_TOKEN) ?: return null
|
||||
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
val tokens = prefs.getStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, emptySet())?.toMutableSet()
|
||||
?: return null
|
||||
if (!tokens.remove(token)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val alwaysAllowPackage = prefs.getString("$KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX$token", null)
|
||||
prefs.edit()
|
||||
.putStringSet(KEY_NOTIFICATION_APPROVAL_TOKENS, tokens)
|
||||
.remove("$KEY_NOTIFICATION_APPROVAL_PACKAGE_PREFIX$token")
|
||||
.apply()
|
||||
|
||||
return NotificationApproval(alwaysAllowPackage)
|
||||
}
|
||||
|
||||
private fun convertToPigeonIntent(intent: Intent, notificationApproval: NotificationApproval?): PigeonIntent {
|
||||
val action = intent.action
|
||||
val data = intent.dataString
|
||||
val fromPackageName = resolveCallerPackage(intent)
|
||||
val fromPackageName = resolveCallerPackage(intent, notificationApproval)
|
||||
|
||||
val categories = ArrayList<String>()
|
||||
intent.categories?.let {
|
||||
@@ -193,6 +227,13 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
||||
intent.extras?.let { bundle ->
|
||||
for (key in bundle.keySet()) {
|
||||
try {
|
||||
if (key == EXTRA_NOTIFICATION_APPROVAL_TOKEN) {
|
||||
continue
|
||||
}
|
||||
if (key == EXTRA_ALWAYS_ALLOW_PACKAGE) {
|
||||
continue
|
||||
}
|
||||
|
||||
when (val value = bundle.get(key)) {
|
||||
is Bundle -> {
|
||||
val bundleMap = HashMap<String, Any?>()
|
||||
@@ -224,6 +265,10 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
||||
}
|
||||
}
|
||||
|
||||
notificationApproval?.alwaysAllowPackage?.let {
|
||||
extras[EXTRA_ALWAYS_ALLOW_PACKAGE] = it
|
||||
}
|
||||
|
||||
return PigeonIntent(
|
||||
fromPackageName = fromPackageName,
|
||||
action = action,
|
||||
|
||||
+46
@@ -309,6 +309,19 @@ interface IntentGatekeeperHostApi {
|
||||
* label cannot be resolved.
|
||||
*/
|
||||
fun resolvePackageLabel(packageName: String): String?
|
||||
/**
|
||||
* Returns the list of packages for which the user tapped "Always allow"
|
||||
* via a blocked-intent notification while WebLibre was not running.
|
||||
* Callers must acknowledge persisted packages via
|
||||
* [ackPendingAlwaysAllows] after Flutter settings were updated
|
||||
* successfully.
|
||||
*/
|
||||
fun getPendingAlwaysAllows(): List<String>
|
||||
/**
|
||||
* Removes the given packages from the pending "Always allow" set after
|
||||
* Flutter has successfully persisted them into its own policy store.
|
||||
*/
|
||||
fun ackPendingAlwaysAllows(packageNames: List<String>)
|
||||
|
||||
companion object {
|
||||
/** The codec used by IntentGatekeeperHostApi. */
|
||||
@@ -355,6 +368,39 @@ interface IntentGatekeeperHostApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getPendingAlwaysAllows())
|
||||
} catch (exception: Throwable) {
|
||||
IntentPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val packageNamesArg = args[0] as List<String>
|
||||
val wrapped: List<Any?> = try {
|
||||
api.ackPendingAlwaysAllows(packageNamesArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
IntentPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,4 +312,50 @@ class IntentGatekeeperHostApi {
|
||||
);
|
||||
return pigeonVar_replyValue as String?;
|
||||
}
|
||||
|
||||
/// Returns the list of packages for which the user tapped "Always allow"
|
||||
/// via a blocked-intent notification while WebLibre was not running.
|
||||
/// Callers must acknowledge persisted packages via
|
||||
/// [ackPendingAlwaysAllows] after Flutter settings were updated
|
||||
/// successfully.
|
||||
Future<List<String>> getPendingAlwaysAllows() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
|
||||
}
|
||||
|
||||
/// Removes the given packages from the pending "Always allow" set after
|
||||
/// Flutter has successfully persisted them into its own policy store.
|
||||
Future<void> ackPendingAlwaysAllows(List<String> packageNames) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[packageNames],
|
||||
);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,15 @@ abstract class IntentGatekeeperHostApi {
|
||||
/// [PackageManager]. Returns `null` if the package is not installed or the
|
||||
/// label cannot be resolved.
|
||||
String? resolvePackageLabel(String packageName);
|
||||
|
||||
/// Returns the list of packages for which the user tapped "Always allow"
|
||||
/// via a blocked-intent notification while WebLibre was not running.
|
||||
/// Callers must acknowledge persisted packages via
|
||||
/// [ackPendingAlwaysAllows] after Flutter settings were updated
|
||||
/// successfully.
|
||||
List<String> getPendingAlwaysAllows();
|
||||
|
||||
/// Removes the given packages from the pending "Always allow" set after
|
||||
/// Flutter has successfully persisted them into its own policy store.
|
||||
void ackPendingAlwaysAllows(List<String> packageNames);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user