added gatekeeper notification options
This commit is contained in:
@@ -60,6 +60,15 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:launchMode="singleTop" />
|
android:launchMode="singleTop" />
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="eu.weblibre.flutter_mozilla_components.gatekeeper.GatekeeperNotificationActionReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.weblibre.gecko.gatekeeper.ALLOW_ONCE" />
|
||||||
|
<action android:name="eu.weblibre.gecko.gatekeeper.ALWAYS_ALLOW" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="org.ironfoxoss.unifiedpush.PushReceiver"
|
android:name="org.ironfoxoss.unifiedpush.PushReceiver"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|||||||
+14
-4
@@ -20,16 +20,16 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/about/domain/providers.dart';
|
||||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/pending_intent_decision.dart';
|
import 'package:weblibre/features/intent_gatekeeper/domain/entities/pending_intent_decision.dart';
|
||||||
|
import 'package:weblibre/features/intent_gatekeeper/domain/services/native_gatekeeper_replicator.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
part 'intent_gatekeeper.g.dart';
|
part 'intent_gatekeeper.g.dart';
|
||||||
|
|
||||||
const _ownPackageName = 'eu.weblibre.gecko';
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class IntentGatekeeper extends _$IntentGatekeeper {
|
class IntentGatekeeper extends _$IntentGatekeeper {
|
||||||
late StreamController<PendingIntentDecision> _decisionRequests;
|
late StreamController<PendingIntentDecision> _decisionRequests;
|
||||||
@@ -62,14 +62,24 @@ class IntentGatekeeper extends _$IntentGatekeeper {
|
|||||||
required String? fromPackageName,
|
required String? fromPackageName,
|
||||||
required String? url,
|
required String? url,
|
||||||
}) async {
|
}) async {
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
await ref
|
||||||
|
.read(nativeIntentGatekeeperReplicatorProvider.notifier)
|
||||||
|
.syncPendingAllows();
|
||||||
|
|
||||||
|
final settings = await ref
|
||||||
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
|
.fetchSettings();
|
||||||
|
|
||||||
if (!settings.blockExternalAppsEnabled) {
|
if (!settings.blockExternalAppsEnabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ownPackageName = (await ref.read(
|
||||||
|
packageInfoProvider.future,
|
||||||
|
)).packageName;
|
||||||
|
|
||||||
// Internal / unknown callers: no package to gate on — let through.
|
// Internal / unknown callers: no package to gate on — let through.
|
||||||
if (fromPackageName == null || fromPackageName == _ownPackageName) {
|
if (fromPackageName == null || fromPackageName == ownPackageName) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-1
@@ -26,6 +26,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:simple_intent_receiver/simple_intent_receiver.dart';
|
import 'package:simple_intent_receiver/simple_intent_receiver.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
part 'native_gatekeeper_replicator.g.dart';
|
part 'native_gatekeeper_replicator.g.dart';
|
||||||
@@ -34,10 +35,14 @@ part 'native_gatekeeper_replicator.g.dart';
|
|||||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||||
/// the Flutter gatekeeper dialog.
|
/// the Flutter gatekeeper dialog.
|
||||||
|
///
|
||||||
|
/// Also consumes any "always allow" decisions made via notification actions
|
||||||
|
/// and merges them into Flutter's policy before the next gatekeeper check.
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class NativeIntentGatekeeperReplicator
|
class NativeIntentGatekeeperReplicator
|
||||||
extends _$NativeIntentGatekeeperReplicator {
|
extends _$NativeIntentGatekeeperReplicator {
|
||||||
final _api = IntentGatekeeperHostApi();
|
final _api = IntentGatekeeperHostApi();
|
||||||
|
Future<void>? _pendingAllowSync;
|
||||||
|
|
||||||
Future<void> _push(
|
Future<void> _push(
|
||||||
({bool enabled, Map<String, IntentSourcePolicy> policies}) config,
|
({bool enabled, Map<String, IntentSourcePolicy> policies}) config,
|
||||||
@@ -58,8 +63,53 @@ class NativeIntentGatekeeperReplicator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads any packages the user approved via notification and persists them
|
||||||
|
/// as `allow` policies in the Flutter settings.
|
||||||
|
Future<void> _syncPendingAllowsInternal() async {
|
||||||
|
try {
|
||||||
|
final pending = await _api.getPendingAlwaysAllows();
|
||||||
|
if (pending.isEmpty) return;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(current) => current.copyWith.externalAppIntentPolicies({
|
||||||
|
...current.externalAppIntentPolicies,
|
||||||
|
for (final pkg in pending) pkg: IntentSourcePolicy.allow,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await _api.ackPendingAlwaysAllows(pending);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
logger.e(
|
||||||
|
'Failed to consume pending always-allows from native',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deduplicates native pending-allow synchronization across concurrent
|
||||||
|
/// callers so startup intent handling and config replication observe the same
|
||||||
|
/// persisted policy state.
|
||||||
|
Future<void> syncPendingAllows() {
|
||||||
|
final inFlight = _pendingAllowSync;
|
||||||
|
if (inFlight != null) {
|
||||||
|
return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
final future = _syncPendingAllowsInternal();
|
||||||
|
_pendingAllowSync = future.whenComplete(() {
|
||||||
|
if (identical(_pendingAllowSync, future)) {
|
||||||
|
_pendingAllowSync = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return _pendingAllowSync!;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
void build() {
|
||||||
|
unawaited(syncPendingAllows());
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
generalSettingsWithDefaultsProvider.select(
|
generalSettingsWithDefaultsProvider.select(
|
||||||
(settings) => EquatableValue((
|
(settings) => EquatableValue((
|
||||||
@@ -80,7 +130,16 @@ class NativeIntentGatekeeperReplicator
|
|||||||
)) {
|
)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unawaited(_push(next));
|
unawaited(() async {
|
||||||
|
await syncPendingAllows();
|
||||||
|
final settings = await ref
|
||||||
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
|
.fetchSettings();
|
||||||
|
await _push((
|
||||||
|
enabled: settings.blockExternalAppsEnabled,
|
||||||
|
policies: settings.externalAppIntentPolicies,
|
||||||
|
));
|
||||||
|
}());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -12,6 +12,9 @@ part of 'native_gatekeeper_replicator.dart';
|
|||||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||||
/// the Flutter gatekeeper dialog.
|
/// the Flutter gatekeeper dialog.
|
||||||
|
///
|
||||||
|
/// On startup, also consumes any "always allow" decisions made via notification
|
||||||
|
/// actions while Flutter was not running, and merges them into Flutter's policy.
|
||||||
|
|
||||||
@ProviderFor(NativeIntentGatekeeperReplicator)
|
@ProviderFor(NativeIntentGatekeeperReplicator)
|
||||||
final nativeIntentGatekeeperReplicatorProvider =
|
final nativeIntentGatekeeperReplicatorProvider =
|
||||||
@@ -21,12 +24,18 @@ final nativeIntentGatekeeperReplicatorProvider =
|
|||||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||||
/// the Flutter gatekeeper dialog.
|
/// the Flutter gatekeeper dialog.
|
||||||
|
///
|
||||||
|
/// On startup, also consumes any "always allow" decisions made via notification
|
||||||
|
/// actions while Flutter was not running, and merges them into Flutter's policy.
|
||||||
final class NativeIntentGatekeeperReplicatorProvider
|
final class NativeIntentGatekeeperReplicatorProvider
|
||||||
extends $NotifierProvider<NativeIntentGatekeeperReplicator, void> {
|
extends $NotifierProvider<NativeIntentGatekeeperReplicator, void> {
|
||||||
/// Mirrors the Flutter-side block list to the native side so the
|
/// Mirrors the Flutter-side block list to the native side so the
|
||||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||||
/// the Flutter gatekeeper dialog.
|
/// the Flutter gatekeeper dialog.
|
||||||
|
///
|
||||||
|
/// On startup, also consumes any "always allow" decisions made via notification
|
||||||
|
/// actions while Flutter was not running, and merges them into Flutter's policy.
|
||||||
NativeIntentGatekeeperReplicatorProvider._()
|
NativeIntentGatekeeperReplicatorProvider._()
|
||||||
: super(
|
: super(
|
||||||
from: null,
|
from: null,
|
||||||
@@ -56,12 +65,15 @@ final class NativeIntentGatekeeperReplicatorProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$nativeIntentGatekeeperReplicatorHash() =>
|
String _$nativeIntentGatekeeperReplicatorHash() =>
|
||||||
r'ee97dbd489e4e946e0a98cd640300f939f3b0682';
|
r'ed1ba2a318467317d6fb412495171acb8819e483';
|
||||||
|
|
||||||
/// Mirrors the Flutter-side block list to the native side so the
|
/// Mirrors the Flutter-side block list to the native side so the
|
||||||
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
/// `IntentReceiverActivity` can reject intents without launching Flutter.
|
||||||
/// Only blocked packages are replicated — allow/unknown still fall through to
|
/// Only blocked packages are replicated — allow/unknown still fall through to
|
||||||
/// the Flutter gatekeeper dialog.
|
/// the Flutter gatekeeper dialog.
|
||||||
|
///
|
||||||
|
/// On startup, also consumes any "always allow" decisions made via notification
|
||||||
|
/// actions while Flutter was not running, and merges them into Flutter's policy.
|
||||||
|
|
||||||
abstract class _$NativeIntentGatekeeperReplicator extends $Notifier<void> {
|
abstract class _$NativeIntentGatekeeperReplicator extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -27,16 +27,34 @@ import 'package:simple_intent_receiver/simple_intent_receiver.dart';
|
|||||||
import 'package:uri_to_file/uri_to_file.dart' as uri_to_file;
|
import 'package:uri_to_file/uri_to_file.dart' as uri_to_file;
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/data/models/received_intent_parameter.dart';
|
import 'package:weblibre/data/models/received_intent_parameter.dart';
|
||||||
|
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
|
||||||
import 'package:weblibre/features/intent_gatekeeper/domain/services/intent_gatekeeper.dart';
|
import 'package:weblibre/features/intent_gatekeeper/domain/services/intent_gatekeeper.dart';
|
||||||
import 'package:weblibre/features/share_intent/domain/entities/intent_container_mode.dart';
|
import 'package:weblibre/features/share_intent/domain/entities/intent_container_mode.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
part 'sharing_intent.g.dart';
|
part 'sharing_intent.g.dart';
|
||||||
|
|
||||||
|
const _alwaysAllowPackageExtra = 'eu.weblibre.gatekeeper.always_allow_package';
|
||||||
|
|
||||||
StreamTransformer<Intent, ReceivedIntentParameter>
|
StreamTransformer<Intent, ReceivedIntentParameter>
|
||||||
_buildSharingIntentTransformer(
|
_buildSharingIntentTransformer(
|
||||||
IntentGatekeeper gatekeeper,
|
IntentGatekeeper gatekeeper,
|
||||||
) => StreamTransformer<Intent, ReceivedIntentParameter>.fromHandlers(
|
) => StreamTransformer<Intent, ReceivedIntentParameter>.fromHandlers(
|
||||||
handleData: (intent, sink) async {
|
handleData: (intent, sink) async {
|
||||||
|
final alwaysAllowPackage =
|
||||||
|
intent.extra[_alwaysAllowPackageExtra] as String?;
|
||||||
|
if (alwaysAllowPackage != null) {
|
||||||
|
await gatekeeper.ref
|
||||||
|
.read(generalSettingsRepositoryProvider.notifier)
|
||||||
|
.updateSettings(
|
||||||
|
(current) => current.copyWith.externalAppIntentPolicies({
|
||||||
|
...current.externalAppIntentPolicies,
|
||||||
|
alwaysAllowPackage: IntentSourcePolicy.allow,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final shortcutContextId = intent.action == 'android.intent.action.VIEW'
|
final shortcutContextId = intent.action == 'android.intent.action.VIEW'
|
||||||
? intent.extra['pwa_context_id'] as String?
|
? intent.extra['pwa_context_id'] as String?
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
+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.PwaSessionCreator
|
||||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentBlockNotifier
|
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentBlockNotifier
|
||||||
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences
|
import eu.weblibre.flutter_mozilla_components.gatekeeper.IntentGatekeeperPreferences
|
||||||
|
import eu.weblibre.flutter_mozilla_components.gatekeeper.GatekeeperNotificationActionReceiver
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -84,13 +85,20 @@ class IntentReceiverActivity : Activity() {
|
|||||||
private fun shouldBlockIntent(intent: Intent): Boolean {
|
private fun shouldBlockIntent(intent: Intent): Boolean {
|
||||||
if (!IntentGatekeeperPreferences.isEnabled(applicationContext)) return false
|
if (!IntentGatekeeperPreferences.isEnabled(applicationContext)) return false
|
||||||
if (intent.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)) 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
|
val caller = resolveCallerPackage(intent) ?: return false
|
||||||
if (caller == packageName) return false
|
if (caller == packageName) return false
|
||||||
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false
|
||||||
|
|
||||||
Log.i(TAG, "Blocking intent from $caller (native gatekeeper)")
|
Log.i(TAG, "Blocking intent from $caller (native gatekeeper)")
|
||||||
IntentBlockNotifier.notifyBlocked(applicationContext, caller)
|
IntentBlockNotifier.notifyBlocked(applicationContext, caller, intent)
|
||||||
return true
|
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
|
package eu.weblibre.flutter_mozilla_components.gatekeeper
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -17,22 +18,25 @@ import androidx.core.content.ContextCompat
|
|||||||
import eu.weblibre.flutter_mozilla_components.R
|
import eu.weblibre.flutter_mozilla_components.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Posts a purely informational notification when an intent is blocked by the
|
* Posts an actionable heads-up notification when an intent is blocked by the
|
||||||
* gatekeeper. The notification has no actions and no content intent.
|
* native gatekeeper. The notification shows "Allow once" and "Always allow"
|
||||||
|
* action buttons, and auto-dismisses after [TIMEOUT_MS] milliseconds.
|
||||||
*/
|
*/
|
||||||
object IntentBlockNotifier {
|
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_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
|
val appCtx = context.applicationContext
|
||||||
ensureChannel(appCtx)
|
ensureChannel(appCtx)
|
||||||
|
|
||||||
val label = resolveAppLabel(appCtx, packageName) ?: packageName
|
val label = resolveAppLabel(appCtx, packageName) ?: packageName
|
||||||
val notificationId = (System.currentTimeMillis() and 0x7FFFFFFF).toInt()
|
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)
|
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||||
.setContentTitle("Blocked app launch")
|
.setContentTitle("Blocked app launch")
|
||||||
.setContentText("Prevented $label from opening WebLibre.")
|
.setContentText("Prevented $label from opening WebLibre.")
|
||||||
@@ -40,15 +44,66 @@ object IntentBlockNotifier {
|
|||||||
NotificationCompat.BigTextStyle()
|
NotificationCompat.BigTextStyle()
|
||||||
.bigText("Prevented $label from opening WebLibre.")
|
.bigText("Prevented $label from opening WebLibre.")
|
||||||
)
|
)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setShowWhen(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)
|
val manager = ContextCompat.getSystemService(appCtx, NotificationManager::class.java)
|
||||||
?: return
|
?: 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) {
|
private fun ensureChannel(context: Context) {
|
||||||
@@ -56,14 +111,16 @@ object IntentBlockNotifier {
|
|||||||
val manager = ContextCompat.getSystemService(context, NotificationManager::class.java)
|
val manager = ContextCompat.getSystemService(context, NotificationManager::class.java)
|
||||||
?: return
|
?: return
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
||||||
|
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
CHANNEL_NAME,
|
CHANNEL_NAME,
|
||||||
NotificationManager.IMPORTANCE_DEFAULT,
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
).apply {
|
).apply {
|
||||||
description = CHANNEL_DESC
|
description = CHANNEL_DESC
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
}
|
}
|
||||||
manager.createNotificationChannel(channel)
|
manager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|||||||
+80
@@ -8,6 +8,7 @@ package eu.weblibre.flutter_mozilla_components.gatekeeper
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cross-package shared-prefs file used to replicate the Flutter-side intent
|
* 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 PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||||
const val KEY_ENABLED = "enabled"
|
const val KEY_ENABLED = "enabled"
|
||||||
const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
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 =
|
fun get(context: Context): SharedPreferences =
|
||||||
context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
@@ -34,4 +42,76 @@ object IntentGatekeeperPreferences {
|
|||||||
val blocked = prefs.getStringSet(KEY_BLOCKED_PACKAGES, emptySet()) ?: return false
|
val blocked = prefs.getStringSet(KEY_BLOCKED_PACKAGES, emptySet()) ?: return false
|
||||||
return packageName in blocked
|
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 PREFS_NAME = "weblibre_intent_gatekeeper"
|
||||||
private const val KEY_ENABLED = "enabled"
|
private const val KEY_ENABLED = "enabled"
|
||||||
private const val KEY_BLOCKED_PACKAGES = "blocked_packages"
|
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>) {
|
override fun setConfig(enabled: Boolean, blockedPackages: List<String>) {
|
||||||
@@ -50,4 +51,24 @@ class IntentGatekeeperHostApiImpl(private val context: Context) : IntentGatekeep
|
|||||||
null
|
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
|
import eu.weblibre.simple_intent_receiver.pigeons.IntentGatekeeperHostApi
|
||||||
|
|
||||||
class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.NewIntentListener {
|
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 lateinit var context: Context
|
||||||
private var intentReceiver: IntentReceiver? = null
|
private var intentReceiver: IntentReceiver? = null
|
||||||
private var lastHandledIntent: String? = null
|
private var lastHandledIntent: String? = null
|
||||||
@@ -125,12 +136,17 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pigeonIntent = convertToPigeonIntent(intent)
|
val notificationApproval = consumeNotificationApproval(intent)
|
||||||
|
val pigeonIntent = convertToPigeonIntent(intent, notificationApproval)
|
||||||
intentReceiver?.sendIntent(pigeonIntent)
|
intentReceiver?.sendIntent(pigeonIntent)
|
||||||
return true
|
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
|
val raw = resolveRawCallerPackage(intent) ?: return null
|
||||||
// Treat system packages (launcher, shell, SystemUI, etc.) as internal — the
|
// Treat system packages (launcher, shell, SystemUI, etc.) as internal — the
|
||||||
// gatekeeper shouldn't prompt the user when the OS itself forwards an intent.
|
// 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 action = intent.action
|
||||||
val data = intent.dataString
|
val data = intent.dataString
|
||||||
val fromPackageName = resolveCallerPackage(intent)
|
val fromPackageName = resolveCallerPackage(intent, notificationApproval)
|
||||||
|
|
||||||
val categories = ArrayList<String>()
|
val categories = ArrayList<String>()
|
||||||
intent.categories?.let {
|
intent.categories?.let {
|
||||||
@@ -193,6 +227,13 @@ class SimpleIntentReceiverPlugin: FlutterPlugin, ActivityAware, PluginRegistry.N
|
|||||||
intent.extras?.let { bundle ->
|
intent.extras?.let { bundle ->
|
||||||
for (key in bundle.keySet()) {
|
for (key in bundle.keySet()) {
|
||||||
try {
|
try {
|
||||||
|
if (key == EXTRA_NOTIFICATION_APPROVAL_TOKEN) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (key == EXTRA_ALWAYS_ALLOW_PACKAGE) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
when (val value = bundle.get(key)) {
|
when (val value = bundle.get(key)) {
|
||||||
is Bundle -> {
|
is Bundle -> {
|
||||||
val bundleMap = HashMap<String, Any?>()
|
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(
|
return PigeonIntent(
|
||||||
fromPackageName = fromPackageName,
|
fromPackageName = fromPackageName,
|
||||||
action = action,
|
action = action,
|
||||||
|
|||||||
+46
@@ -309,6 +309,19 @@ interface IntentGatekeeperHostApi {
|
|||||||
* label cannot be resolved.
|
* label cannot be resolved.
|
||||||
*/
|
*/
|
||||||
fun resolvePackageLabel(packageName: String): String?
|
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 {
|
companion object {
|
||||||
/** The codec used by IntentGatekeeperHostApi. */
|
/** The codec used by IntentGatekeeperHostApi. */
|
||||||
@@ -355,6 +368,39 @@ interface IntentGatekeeperHostApi {
|
|||||||
channel.setMessageHandler(null)
|
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?;
|
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
|
/// [PackageManager]. Returns `null` if the package is not installed or the
|
||||||
/// label cannot be resolved.
|
/// label cannot be resolved.
|
||||||
String? resolvePackageLabel(String packageName);
|
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