intent gatekeeper initial
This commit is contained in:
+55
@@ -11,12 +11,15 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import eu.weblibre.flutter_mozilla_components.Components
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentBlockNotifier
|
||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -58,9 +61,55 @@ class IntentReceiverActivity : Activity() {
|
||||
intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
|
||||
intent.flags = intent.flags and Intent.FLAG_ACTIVITY_CLEAR_TASK.inv()
|
||||
|
||||
if (shouldBlockIntent(intent)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
processIntent(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast native block-check. Only rejects packages explicitly on the blocked
|
||||
* list; allowed and unknown packages fall through to the Flutter-side
|
||||
* gatekeeper which can still prompt the user.
|
||||
*
|
||||
* PWA launches carrying our trusted profile metadata are never blocked here —
|
||||
* those are treated as internal launches regardless of the caller.
|
||||
*/
|
||||
private fun shouldBlockIntent(intent: Intent): Boolean {
|
||||
if (!IntentGatekeeperPreferences.isEnabled(applicationContext)) return false
|
||||
if (intent.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)) 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)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveCallerPackage(intent: Intent): String? {
|
||||
referrer?.let { uri ->
|
||||
if (uri.scheme == "android-app") {
|
||||
uri.host?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
if (referrerUri?.scheme == "android-app") {
|
||||
referrerUri.host?.let { return it }
|
||||
}
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
||||
Uri.parse(name).takeIf { it.scheme == "android-app" }?.host?.let { return it }
|
||||
}
|
||||
|
||||
return callingPackage
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
@@ -439,6 +488,12 @@ class IntentReceiverActivity : Activity() {
|
||||
val mainActivityIntent = Intent(intent).apply {
|
||||
setClassName(this@IntentReceiverActivity, "eu.weblibre.gecko.MainActivity")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// Preserve the original caller so the gatekeeper on the Flutter side
|
||||
// can identify which app triggered this intent (getReferrer() in the
|
||||
// forwarded activity would otherwise resolve to ourselves).
|
||||
if (!hasExtra(Intent.EXTRA_REFERRER) && !hasExtra(Intent.EXTRA_REFERRER_NAME)) {
|
||||
referrer?.let { putExtra(Intent.EXTRA_REFERRER, it) }
|
||||
}
|
||||
}
|
||||
startActivity(mainActivityIntent)
|
||||
finish()
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
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.
|
||||
*/
|
||||
object IntentBlockNotifier {
|
||||
private const val CHANNEL_ID = "intent_gatekeeper_channel"
|
||||
private const val CHANNEL_NAME = "Blocked app launches"
|
||||
private const val CHANNEL_DESC = "Informs you when another app is prevented from opening WebLibre."
|
||||
|
||||
fun notifyBlocked(context: Context, packageName: String) {
|
||||
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)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle("Blocked app launch")
|
||||
.setContentText("Prevented $label from opening WebLibre.")
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText("Prevented $label from opening WebLibre.")
|
||||
)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setSilent(true)
|
||||
.setAutoCancel(true)
|
||||
.setShowWhen(true)
|
||||
.build()
|
||||
|
||||
val manager = ContextCompat.getSystemService(appCtx, NotificationManager::class.java)
|
||||
?: return
|
||||
manager.notify(notificationId, notification)
|
||||
}
|
||||
|
||||
private fun ensureChannel(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
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,
|
||||
).apply {
|
||||
description = CHANNEL_DESC
|
||||
setShowBadge(false)
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun resolveAppLabel(context: Context, packageName: String): String? {
|
||||
return try {
|
||||
val pm = context.packageManager
|
||||
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getApplicationInfo(
|
||||
packageName,
|
||||
PackageManager.ApplicationInfoFlags.of(0),
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getApplicationInfo(packageName, 0)
|
||||
}
|
||||
pm.getApplicationLabel(info).toString()
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
/**
|
||||
* Cross-package shared-prefs file used to replicate the Flutter-side intent
|
||||
* gatekeeper policy to the native side so [IntentReceiverActivity] can block
|
||||
* intents without launching Flutter.
|
||||
*
|
||||
* The file name is a stable constant: other packages (e.g. simple_intent_receiver)
|
||||
* write to the same file using [Context.getSharedPreferences] with this name.
|
||||
*/
|
||||
object IntentGatekeeperPreferences {
|
||||
const val PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||
const val KEY_ENABLED = "enabled"
|
||||
const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
||||
|
||||
fun get(context: Context): SharedPreferences =
|
||||
context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
fun isEnabled(context: Context): Boolean =
|
||||
get(context).getBoolean(KEY_ENABLED, false)
|
||||
|
||||
fun isBlocked(context: Context, packageName: String): Boolean {
|
||||
val prefs = get(context)
|
||||
if (!prefs.getBoolean(KEY_ENABLED, false)) return false
|
||||
val blocked = prefs.getStringSet(KEY_BLOCKED_PACKAGES, emptySet()) ?: return false
|
||||
return packageName in blocked
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*/
|
||||
package eu.weblibre.simple_intent_receiver
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import eu.weblibre.simple_intent_receiver.pigeons.IntentGatekeeperHostApi
|
||||
|
||||
/**
|
||||
* Persists the Flutter-side gatekeeper policy to a shared-prefs file that
|
||||
* [eu.weblibre.flutter_mozilla_components.activities.IntentReceiverActivity]
|
||||
* reads on each incoming intent.
|
||||
*
|
||||
* The prefs file name MUST match
|
||||
* [eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences.PREFS_NAME].
|
||||
*/
|
||||
class IntentGatekeeperHostApiImpl(private val context: Context) : IntentGatekeeperHostApi {
|
||||
companion object {
|
||||
private const val PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||
private const val KEY_ENABLED = "enabled"
|
||||
private const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
||||
}
|
||||
|
||||
override fun setConfig(enabled: Boolean, blockedPackages: List<String>) {
|
||||
val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit()
|
||||
.putBoolean(KEY_ENABLED, enabled)
|
||||
.putStringSet(KEY_BLOCKED_PACKAGES, blockedPackages.toSet())
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun resolvePackageLabel(packageName: String): String? {
|
||||
return try {
|
||||
val pm = context.applicationContext.packageManager
|
||||
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getApplicationInfo(packageName, 0)
|
||||
}
|
||||
pm.getApplicationLabel(info).toString()
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
-1
@@ -22,7 +22,10 @@ package eu.weblibre.simple_intent_receiver
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import io.flutter.Log
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
@@ -30,20 +33,29 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
import eu.weblibre.simple_intent_receiver.pigeons.Intent as PigeonIntent
|
||||
import eu.weblibre.simple_intent_receiver.pigeons.IntentGatekeeperHostApi
|
||||
|
||||
class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener {
|
||||
private lateinit var context: Context
|
||||
private var intentReceiver: IntentReceiver? = null
|
||||
private var lastHandledIntent: String? = null
|
||||
private var activity: Activity? = null
|
||||
private var binaryMessenger: io.flutter.plugin.common.BinaryMessenger? = null
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
context = flutterPluginBinding.applicationContext
|
||||
intentReceiver = IntentReceiver(flutterPluginBinding.binaryMessenger)
|
||||
binaryMessenger = flutterPluginBinding.binaryMessenger
|
||||
IntentGatekeeperHostApi.setUp(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
IntentGatekeeperHostApiImpl(flutterPluginBinding.applicationContext),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
intentReceiver = null
|
||||
binaryMessenger?.let { IntentGatekeeperHostApi.setUp(it, null) }
|
||||
binaryMessenger = null
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
@@ -118,10 +130,59 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveCallerPackage(intent: Intent): String? {
|
||||
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.
|
||||
if (isSystemPackage(raw)) return null
|
||||
return raw
|
||||
}
|
||||
|
||||
private fun resolveRawCallerPackage(intent: Intent): String? {
|
||||
// 1. Try Activity.getReferrer() — handles EXTRA_REFERRER/_NAME and real caller.
|
||||
activity?.referrer?.let { uri ->
|
||||
if (uri.scheme == "android-app") {
|
||||
uri.host?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to explicit referrer extras on the intent itself.
|
||||
@Suppress("DEPRECATION")
|
||||
val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER)
|
||||
if (referrerUri?.scheme == "android-app") {
|
||||
referrerUri.host?.let { return it }
|
||||
}
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name ->
|
||||
Uri.parse(name).takeIf { it.scheme == "android-app" }?.host?.let { return it }
|
||||
}
|
||||
|
||||
// 3. Caller for startActivityForResult flows.
|
||||
return activity?.callingPackage
|
||||
}
|
||||
|
||||
private fun isSystemPackage(packageName: String): Boolean {
|
||||
return try {
|
||||
val pm = context.packageManager
|
||||
val info = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm.getApplicationInfo(packageName, 0)
|
||||
}
|
||||
val systemFlags = ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
|
||||
(info.flags and systemFlags) != 0
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
false
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToPigeonIntent(intent: Intent): PigeonIntent {
|
||||
val action = intent.action
|
||||
val data = intent.dataString
|
||||
val fromPackageName = intent.getPackage()
|
||||
val fromPackageName = resolveCallerPackage(intent)
|
||||
|
||||
val categories = ArrayList<String>()
|
||||
intent.categories?.let {
|
||||
|
||||
+82
@@ -17,6 +17,26 @@ private object IntentPigeonUtils {
|
||||
|
||||
fun createConnectionError(channelName: String): FlutterError {
|
||||
return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
fun doubleEquals(a: Double, b: Double): Boolean {
|
||||
// Normalize -0.0 to 0.0 and handle NaN equality.
|
||||
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
|
||||
@@ -276,3 +296,65 @@ class IntentEvents(private val binaryMessenger: BinaryMessenger, private val mes
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface IntentGatekeeperHostApi {
|
||||
/**
|
||||
* Replicates the blocked-packages policy to the native side so the
|
||||
* [IntentReceiverActivity] can reject intents without launching Flutter.
|
||||
*/
|
||||
fun setConfig(enabled: Boolean, blockedPackages: List<String>)
|
||||
/**
|
||||
* Resolves a package name to its user-visible application label via
|
||||
* [PackageManager]. Returns `null` if the package is not installed or the
|
||||
* label cannot be resolved.
|
||||
*/
|
||||
fun resolvePackageLabel(packageName: String): String?
|
||||
|
||||
companion object {
|
||||
/** The codec used by IntentGatekeeperHostApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
IntentPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `IntentGatekeeperHostApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: IntentGatekeeperHostApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val enabledArg = args[0] as Boolean
|
||||
val blockedPackagesArg = args[1] as List<String>
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setConfig(enabledArg, blockedPackagesArg)
|
||||
listOf(null)
|
||||
} 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.resolvePackageLabel$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val packageNameArg = args[0] as String
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.resolvePackageLabel(packageNameArg))
|
||||
} catch (exception: Throwable) {
|
||||
IntentPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export 'src/intent_receiver.dart';
|
||||
export 'src/pigeons/intent.g.dart' show Intent;
|
||||
export 'src/pigeons/intent.g.dart' show Intent, IntentGatekeeperHostApi;
|
||||
|
||||
@@ -9,6 +9,32 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
}
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
@@ -204,3 +230,59 @@ abstract class IntentEvents {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IntentGatekeeperHostApi {
|
||||
/// Constructor for [IntentGatekeeperHostApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
IntentGatekeeperHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
/// Replicates the blocked-packages policy to the native side so the
|
||||
/// [IntentReceiverActivity] can reject intents without launching Flutter.
|
||||
Future<void> setConfig(bool enabled, List<String> blockedPackages) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[enabled, blockedPackages]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/// Resolves a package name to its user-visible application label via
|
||||
/// [PackageManager]. Returns `null` if the package is not installed or the
|
||||
/// label cannot be resolved.
|
||||
Future<String?> resolvePackageLabel(String packageName) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.resolvePackageLabel$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[packageName]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue as String?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,3 +53,15 @@ class Intent {
|
||||
abstract class IntentEvents {
|
||||
void onIntentReceived(int sequence, Intent intent);
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
abstract class IntentGatekeeperHostApi {
|
||||
/// Replicates the blocked-packages policy to the native side so the
|
||||
/// [IntentReceiverActivity] can reject intents without launching Flutter.
|
||||
void setConfig(bool enabled, List<String> blockedPackages);
|
||||
|
||||
/// Resolves a package name to its user-visible application label via
|
||||
/// [PackageManager]. Returns `null` if the package is not installed or the
|
||||
/// label cannot be resolved.
|
||||
String? resolvePackageLabel(String packageName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user