diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart
new file mode 100644
index 00000000..af455db7
--- /dev/null
+++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2024-2025 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
+import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart';
+
+/// Section widget for clearing site data
+class ClearSiteDataSection extends HookConsumerWidget {
+ final Uri url;
+
+ const ClearSiteDataSection({
+ required this.url,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final isExpanded = useState(false);
+ final isClearing = useState(false);
+ final selectedTypes = ref.watch(selectedClearDataTypesProvider);
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ leading: const Icon(Icons.delete_sweep),
+ title: const Text('Clear Site Data'),
+ subtitle: Text(
+ isExpanded.value
+ ? 'Select data types to clear'
+ : 'Cookies, cache, and site data',
+ ),
+ trailing: Icon(
+ isExpanded.value ? Icons.expand_less : Icons.expand_more,
+ ),
+ onTap: () => isExpanded.value = !isExpanded.value,
+ ),
+ if (isExpanded.value) ...[
+ _DataTypeCheckbox(
+ label: 'Cookies',
+ subtitle: 'Login tokens, preferences, tracking data',
+ type: ClearDataType.cookies,
+ isSelected: selectedTypes.contains(ClearDataType.cookies),
+ onChanged: (selected) {
+ ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.cookies);
+ },
+ ),
+ _DataTypeCheckbox(
+ label: 'Cached Files',
+ subtitle: 'Images, scripts, stylesheets',
+ type: ClearDataType.allCaches,
+ isSelected: selectedTypes.contains(ClearDataType.allCaches),
+ onChanged: (selected) {
+ ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allCaches);
+ },
+ ),
+ _DataTypeCheckbox(
+ label: 'Site Data',
+ subtitle: 'Offline storage, databases, local files',
+ type: ClearDataType.allSiteData,
+ isSelected: selectedTypes.contains(ClearDataType.allSiteData),
+ onChanged: (selected) {
+ ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.allSiteData);
+ },
+ ),
+ _DataTypeCheckbox(
+ label: 'Auth Sessions',
+ subtitle: 'Saved logins, active sessions',
+ type: ClearDataType.authSessions,
+ isSelected: selectedTypes.contains(ClearDataType.authSessions),
+ onChanged: (selected) {
+ ref.read(selectedClearDataTypesProvider.notifier).toggle(ClearDataType.authSessions);
+ },
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: isClearing.value || selectedTypes.isEmpty
+ ? null
+ : () => _showConfirmationAndClear(context, ref, isClearing),
+ icon: isClearing.value
+ ? const SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ : const Icon(Icons.delete),
+ label: Text(isClearing.value ? 'Clearing...' : 'Clear Now'),
+ ),
+ ),
+ ),
+ ],
+ ],
+ );
+ }
+
+ Future _showConfirmationAndClear(
+ BuildContext context,
+ WidgetRef ref,
+ ValueNotifier isClearing,
+ ) async {
+ final selectedTypes = ref.read(selectedClearDataTypesProvider);
+ if (selectedTypes.isEmpty) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Select at least one data type')),
+ );
+ return;
+ }
+
+ final confirmed = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ icon: const Icon(Icons.warning),
+ title: const Text('Clear Site Data'),
+ content: Text(
+ 'This will clear ${_formatTypes(selectedTypes)} for ${url.host}.\n\n'
+ 'You may need to log in again.',
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context, false),
+ child: const Text('Cancel'),
+ ),
+ FilledButton(
+ onPressed: () => Navigator.pop(context, true),
+ child: const Text('Clear'),
+ ),
+ ],
+ ),
+ );
+
+ if (confirmed == true && context.mounted) {
+ isClearing.value = true;
+ try {
+ await _clearData(ref, selectedTypes);
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Site data cleared')),
+ );
+ }
+ } catch (e) {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text('Failed to clear site data: $e')),
+ );
+ }
+ } finally {
+ isClearing.value = false;
+ }
+ }
+ }
+
+ String _formatTypes(Set types) {
+ final labels = types.map((t) {
+ switch (t) {
+ case ClearDataType.cookies:
+ return 'cookies';
+ case ClearDataType.allCaches:
+ return 'cached files';
+ case ClearDataType.allSiteData:
+ return 'site data';
+ case ClearDataType.authSessions:
+ return 'auth sessions';
+ }
+ }).toList();
+
+ if (labels.length == 1) return labels.first;
+ if (labels.length == 2) return '${labels[0]} and ${labels[1]}';
+ return '${labels.sublist(0, labels.length - 1).join(', ')}, and ${labels.last}';
+ }
+
+ Future _clearData(WidgetRef ref, Set selectedTypes) async {
+ final host = url.host;
+
+ // Get base domain using PSL API (falls back to host on error)
+ final pslApi = GeckoPublicSuffixListApi();
+ final baseDomain = await pslApi.getPublicSuffixPlusOne(host);
+
+ // Clear data via API
+ final clearApi = GeckoDeleteBrowsingDataController();
+ await clearApi.clearDataForHost(baseDomain, selectedTypes.toList());
+
+ // Reload tab
+ await ref.read(selectedTabSessionProvider).reload();
+ }
+}
+
+class _DataTypeCheckbox extends StatelessWidget {
+ final String label;
+ final String subtitle;
+ final ClearDataType type;
+ final bool isSelected;
+ final ValueChanged onChanged;
+
+ const _DataTypeCheckbox({
+ required this.label,
+ required this.subtitle,
+ required this.type,
+ required this.isSelected,
+ required this.onChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return CheckboxListTile(
+ title: Text(label),
+ subtitle: Text(subtitle),
+ value: isSelected,
+ onChanged: (value) => onChanged(value ?? false),
+ controlAffinity: ListTileControlAffinity.trailing,
+ );
+ }
+}
diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart
new file mode 100644
index 00000000..fba8b235
--- /dev/null
+++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart
@@ -0,0 +1,492 @@
+/*
+ * Copyright (c) 2024-2025 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
+import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart';
+
+/// Section widget displaying site permissions with toggles
+class PermissionsSection extends HookConsumerWidget {
+ final String origin;
+ final bool isPrivate;
+
+ const PermissionsSection({
+ required this.origin,
+ required this.isPrivate,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final permissionsAsync = ref.watch(
+ sitePermissionsProvider(origin, isPrivate),
+ );
+
+ return permissionsAsync.when(
+ data: (permissions) => _PermissionsList(
+ origin: origin,
+ isPrivate: isPrivate,
+ permissions: permissions,
+ ),
+ loading: () => const Padding(
+ padding: EdgeInsets.all(16.0),
+ child: Center(child: CircularProgressIndicator()),
+ ),
+ error: (error, stack) => Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text('Error loading permissions: $error'),
+ ),
+ );
+ }
+}
+
+class _PermissionsList extends HookConsumerWidget {
+ final String origin;
+ final bool isPrivate;
+ final SitePermissions? permissions;
+
+ const _PermissionsList({
+ required this.origin,
+ required this.isPrivate,
+ required this.permissions,
+ });
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final showAll = useState(false);
+
+ // Build list of permission entries with their current status
+ final allPermissions = [
+ _PermissionEntry(
+ icon: Icons.videocam,
+ label: 'Camera',
+ status: permissions?.camera,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithCamera(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.mic,
+ label: 'Microphone',
+ status: permissions?.microphone,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithMicrophone(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.location_on,
+ label: 'Location',
+ status: permissions?.location,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithLocation(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.notifications,
+ label: 'Notifications',
+ status: permissions?.notification,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithNotification(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.storage,
+ label: 'Persistent Storage',
+ status: permissions?.persistentStorage,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithPersistentStorage(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.cookie,
+ label: 'Cross-Origin Storage',
+ status: permissions?.crossOriginStorageAccess,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithCrossOriginStorage(p, status)),
+ ),
+ _PermissionEntry(
+ icon: Icons.key,
+ label: 'Media Key System (DRM)',
+ status: permissions?.mediaKeySystemAccess,
+ onChanged: (status) => _updatePermission(ref, (p) => _copyWithMediaKeySystem(p, status)),
+ ),
+ ];
+
+ // Filter to only show permissions that have been explicitly set (not noDecision)
+ final setPermissions = allPermissions.where(
+ (p) => p.status != null && p.status != SitePermissionStatus.noDecision,
+ ).toList();
+
+ final permissionsToShow = showAll.value ? allPermissions : setPermissions;
+ final hiddenCount = allPermissions.length - setPermissions.length;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Row(
+ children: [
+ Text(
+ 'Permissions',
+ style: Theme.of(context).textTheme.titleSmall?.copyWith(
+ color: Theme.of(context).colorScheme.primary,
+ ),
+ ),
+ const Spacer(),
+ if (hiddenCount > 0)
+ TextButton(
+ onPressed: () => showAll.value = !showAll.value,
+ child: Text(showAll.value ? 'Show less' : 'Show all'),
+ ),
+ ],
+ ),
+ ),
+ ...permissionsToShow.map((entry) => _PermissionTile(
+ icon: entry.icon,
+ label: entry.label,
+ status: entry.status,
+ onChanged: entry.onChanged,
+ )),
+ if (permissionsToShow.isEmpty && !showAll.value)
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Text(
+ 'No permissions set for this site',
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ),
+ const SizedBox(height: 8.0),
+ _AutoplayTile(
+ audibleStatus: permissions?.autoplayAudible,
+ inaudibleStatus: permissions?.autoplayInaudible,
+ onChanged: (audible, inaudible) => _updateAutoplay(ref, audible, inaudible),
+ ),
+ ],
+ );
+ }
+
+ Future _updatePermission(
+ WidgetRef ref,
+ SitePermissions Function(SitePermissions) updater,
+ ) async {
+ final currentPermissions = permissions ?? SitePermissions(
+ origin: origin,
+ savedAt: DateTime.now().millisecondsSinceEpoch,
+ );
+
+ final updatedPermissions = updater(currentPermissions);
+
+ final api = GeckoSitePermissionsApi();
+ await api.setSitePermissions(updatedPermissions, isPrivate);
+
+ // Invalidate the provider to refetch
+ ref.invalidate(sitePermissionsProvider(origin, isPrivate));
+
+ // Reload the tab to apply changes
+ await ref.read(selectedTabSessionProvider).reload();
+ }
+
+ Future _updateAutoplay(
+ WidgetRef ref,
+ AutoplayStatus? audible,
+ AutoplayStatus? inaudible,
+ ) async {
+ final currentPermissions = permissions ?? SitePermissions(
+ origin: origin,
+ savedAt: DateTime.now().millisecondsSinceEpoch,
+ );
+
+ final updatedPermissions = SitePermissions(
+ origin: currentPermissions.origin,
+ camera: currentPermissions.camera,
+ microphone: currentPermissions.microphone,
+ location: currentPermissions.location,
+ notification: currentPermissions.notification,
+ persistentStorage: currentPermissions.persistentStorage,
+ crossOriginStorageAccess: currentPermissions.crossOriginStorageAccess,
+ mediaKeySystemAccess: currentPermissions.mediaKeySystemAccess,
+ localDeviceAccess: currentPermissions.localDeviceAccess,
+ localNetworkAccess: currentPermissions.localNetworkAccess,
+ autoplayAudible: audible ?? currentPermissions.autoplayAudible,
+ autoplayInaudible: inaudible ?? currentPermissions.autoplayInaudible,
+ savedAt: currentPermissions.savedAt,
+ );
+
+ final api = GeckoSitePermissionsApi();
+ await api.setSitePermissions(updatedPermissions, isPrivate);
+
+ ref.invalidate(sitePermissionsProvider(origin, isPrivate));
+ await ref.read(selectedTabSessionProvider).reload();
+ }
+
+ // Copy helper methods since Pigeon doesn't generate copyWith
+ SitePermissions _copyWithCamera(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: status,
+ microphone: p.microphone,
+ location: p.location,
+ notification: p.notification,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithMicrophone(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: status,
+ location: p.location,
+ notification: p.notification,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithLocation(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: p.microphone,
+ location: status,
+ notification: p.notification,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithNotification(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: p.microphone,
+ location: p.location,
+ notification: status,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithPersistentStorage(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: p.microphone,
+ location: p.location,
+ notification: p.notification,
+ persistentStorage: status,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithCrossOriginStorage(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: p.microphone,
+ location: p.location,
+ notification: p.notification,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: status,
+ mediaKeySystemAccess: p.mediaKeySystemAccess,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+
+ SitePermissions _copyWithMediaKeySystem(SitePermissions p, SitePermissionStatus status) {
+ return SitePermissions(
+ origin: p.origin,
+ camera: p.camera,
+ microphone: p.microphone,
+ location: p.location,
+ notification: p.notification,
+ persistentStorage: p.persistentStorage,
+ crossOriginStorageAccess: p.crossOriginStorageAccess,
+ mediaKeySystemAccess: status,
+ localDeviceAccess: p.localDeviceAccess,
+ localNetworkAccess: p.localNetworkAccess,
+ autoplayAudible: p.autoplayAudible,
+ autoplayInaudible: p.autoplayInaudible,
+ savedAt: p.savedAt,
+ );
+ }
+}
+
+class _PermissionEntry {
+ final IconData icon;
+ final String label;
+ final SitePermissionStatus? status;
+ final ValueChanged onChanged;
+
+ _PermissionEntry({
+ required this.icon,
+ required this.label,
+ required this.status,
+ required this.onChanged,
+ });
+}
+
+class _PermissionTile extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final SitePermissionStatus? status;
+ final ValueChanged onChanged;
+
+ const _PermissionTile({
+ required this.icon,
+ required this.label,
+ required this.status,
+ required this.onChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final currentStatus = status ?? SitePermissionStatus.noDecision;
+
+ return ListTile(
+ leading: Icon(icon),
+ title: Text(label),
+ trailing: DropdownButton(
+ value: currentStatus,
+ underline: const SizedBox(),
+ items: const [
+ DropdownMenuItem(
+ value: SitePermissionStatus.noDecision,
+ child: Text('Ask'),
+ ),
+ DropdownMenuItem(
+ value: SitePermissionStatus.allowed,
+ child: Text('Allow'),
+ ),
+ DropdownMenuItem(
+ value: SitePermissionStatus.blocked,
+ child: Text('Block'),
+ ),
+ ],
+ onChanged: (value) {
+ if (value != null) {
+ onChanged(value);
+ }
+ },
+ ),
+ );
+ }
+}
+
+class _AutoplayTile extends StatelessWidget {
+ final AutoplayStatus? audibleStatus;
+ final AutoplayStatus? inaudibleStatus;
+ final void Function(AutoplayStatus?, AutoplayStatus?) onChanged;
+
+ const _AutoplayTile({
+ required this.audibleStatus,
+ required this.inaudibleStatus,
+ required this.onChanged,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ // Determine the combined autoplay setting
+ final combinedStatus = _getCombinedStatus();
+
+ return ListTile(
+ leading: const Icon(Icons.play_circle),
+ title: const Text('Autoplay'),
+ trailing: DropdownButton<_AutoplayCombined>(
+ value: combinedStatus,
+ underline: const SizedBox(),
+ items: const [
+ DropdownMenuItem(
+ value: _AutoplayCombined.allowAll,
+ child: Text('Allow All'),
+ ),
+ DropdownMenuItem(
+ value: _AutoplayCombined.blockAudible,
+ child: Text('Block Audible'),
+ ),
+ DropdownMenuItem(
+ value: _AutoplayCombined.blockAll,
+ child: Text('Block All'),
+ ),
+ ],
+ onChanged: (value) {
+ if (value == null) return;
+ switch (value) {
+ case _AutoplayCombined.allowAll:
+ onChanged(AutoplayStatus.allowed, AutoplayStatus.allowed);
+ case _AutoplayCombined.blockAudible:
+ onChanged(AutoplayStatus.blocked, AutoplayStatus.allowed);
+ case _AutoplayCombined.blockAll:
+ onChanged(AutoplayStatus.blocked, AutoplayStatus.blocked);
+ }
+ },
+ ),
+ );
+ }
+
+ _AutoplayCombined _getCombinedStatus() {
+ final audible = audibleStatus ?? AutoplayStatus.blocked;
+ final inaudible = inaudibleStatus ?? AutoplayStatus.allowed;
+
+ if (audible == AutoplayStatus.allowed && inaudible == AutoplayStatus.allowed) {
+ return _AutoplayCombined.allowAll;
+ } else if (audible == AutoplayStatus.blocked && inaudible == AutoplayStatus.allowed) {
+ return _AutoplayCombined.blockAudible;
+ } else {
+ return _AutoplayCombined.blockAll;
+ }
+ }
+}
+
+enum _AutoplayCombined {
+ allowAll,
+ blockAudible,
+ blockAll,
+}
diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart
new file mode 100644
index 00000000..01ac5229
--- /dev/null
+++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.dart
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2024-2025 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'site_permissions_provider.g.dart';
+
+/// Provider to fetch site permissions for a given origin
+@Riverpod()
+Future sitePermissions(
+ Ref ref,
+ String origin,
+ bool isPrivate,
+) async {
+ final api = GeckoSitePermissionsApi();
+ return api.getSitePermissions(origin, isPrivate);
+}
+
+/// Provider to get the public suffix plus one (eTLD+1) for a host
+@Riverpod()
+Future publicSuffixPlusOne(
+ Ref ref,
+ String host,
+) async {
+ final api = GeckoPublicSuffixListApi();
+ return api.getPublicSuffixPlusOne(host);
+}
+
+/// Notifier for managing selected clear data types
+@Riverpod()
+class SelectedClearDataTypes extends _$SelectedClearDataTypes {
+ @override
+ Set build() {
+ // Default to clearing all site data and auth sessions (most common use case)
+ return {
+ ClearDataType.allSiteData,
+ ClearDataType.authSessions,
+ };
+ }
+
+ void toggle(ClearDataType type) {
+ if (state.contains(type)) {
+ state = Set.from(state)..remove(type);
+ } else {
+ state = Set.from(state)..add(type);
+ }
+ }
+
+ void selectAll() {
+ state = Set.from(ClearDataType.values);
+ }
+
+ void clearAll() {
+ state = {};
+ }
+}
diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart
new file mode 100644
index 00000000..071ee6da
--- /dev/null
+++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/site_permissions_provider.g.dart
@@ -0,0 +1,231 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'site_permissions_provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+/// Provider to fetch site permissions for a given origin
+
+@ProviderFor(sitePermissions)
+final sitePermissionsProvider = SitePermissionsFamily._();
+
+/// Provider to fetch site permissions for a given origin
+
+final class SitePermissionsProvider
+ extends
+ $FunctionalProvider<
+ AsyncValue,
+ SitePermissions?,
+ FutureOr
+ >
+ with $FutureModifier, $FutureProvider {
+ /// Provider to fetch site permissions for a given origin
+ SitePermissionsProvider._({
+ required SitePermissionsFamily super.from,
+ required (String, bool) super.argument,
+ }) : super(
+ retry: null,
+ name: r'sitePermissionsProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$sitePermissionsHash();
+
+ @override
+ String toString() {
+ return r'sitePermissionsProvider'
+ ''
+ '$argument';
+ }
+
+ @$internal
+ @override
+ $FutureProviderElement $createElement(
+ $ProviderPointer pointer,
+ ) => $FutureProviderElement(pointer);
+
+ @override
+ FutureOr create(Ref ref) {
+ final argument = this.argument as (String, bool);
+ return sitePermissions(ref, argument.$1, argument.$2);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is SitePermissionsProvider && other.argument == argument;
+ }
+
+ @override
+ int get hashCode {
+ return argument.hashCode;
+ }
+}
+
+String _$sitePermissionsHash() => r'c605e9a1acb261ec46659431b5bb1e105b8aa2a1';
+
+/// Provider to fetch site permissions for a given origin
+
+final class SitePermissionsFamily extends $Family
+ with $FunctionalFamilyOverride, (String, bool)> {
+ SitePermissionsFamily._()
+ : super(
+ retry: null,
+ name: r'sitePermissionsProvider',
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ isAutoDispose: true,
+ );
+
+ /// Provider to fetch site permissions for a given origin
+
+ SitePermissionsProvider call(String origin, bool isPrivate) =>
+ SitePermissionsProvider._(argument: (origin, isPrivate), from: this);
+
+ @override
+ String toString() => r'sitePermissionsProvider';
+}
+
+/// Provider to get the public suffix plus one (eTLD+1) for a host
+
+@ProviderFor(publicSuffixPlusOne)
+final publicSuffixPlusOneProvider = PublicSuffixPlusOneFamily._();
+
+/// Provider to get the public suffix plus one (eTLD+1) for a host
+
+final class PublicSuffixPlusOneProvider
+ extends $FunctionalProvider, String, FutureOr>
+ with $FutureModifier, $FutureProvider {
+ /// Provider to get the public suffix plus one (eTLD+1) for a host
+ PublicSuffixPlusOneProvider._({
+ required PublicSuffixPlusOneFamily super.from,
+ required String super.argument,
+ }) : super(
+ retry: null,
+ name: r'publicSuffixPlusOneProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$publicSuffixPlusOneHash();
+
+ @override
+ String toString() {
+ return r'publicSuffixPlusOneProvider'
+ ''
+ '($argument)';
+ }
+
+ @$internal
+ @override
+ $FutureProviderElement $createElement($ProviderPointer pointer) =>
+ $FutureProviderElement(pointer);
+
+ @override
+ FutureOr create(Ref ref) {
+ final argument = this.argument as String;
+ return publicSuffixPlusOne(ref, argument);
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is PublicSuffixPlusOneProvider && other.argument == argument;
+ }
+
+ @override
+ int get hashCode {
+ return argument.hashCode;
+ }
+}
+
+String _$publicSuffixPlusOneHash() =>
+ r'e259f66e3ebf278468223d8e25618eb9f67a3504';
+
+/// Provider to get the public suffix plus one (eTLD+1) for a host
+
+final class PublicSuffixPlusOneFamily extends $Family
+ with $FunctionalFamilyOverride, String> {
+ PublicSuffixPlusOneFamily._()
+ : super(
+ retry: null,
+ name: r'publicSuffixPlusOneProvider',
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ isAutoDispose: true,
+ );
+
+ /// Provider to get the public suffix plus one (eTLD+1) for a host
+
+ PublicSuffixPlusOneProvider call(String host) =>
+ PublicSuffixPlusOneProvider._(argument: host, from: this);
+
+ @override
+ String toString() => r'publicSuffixPlusOneProvider';
+}
+
+/// Notifier for managing selected clear data types
+
+@ProviderFor(SelectedClearDataTypes)
+final selectedClearDataTypesProvider = SelectedClearDataTypesProvider._();
+
+/// Notifier for managing selected clear data types
+final class SelectedClearDataTypesProvider
+ extends $NotifierProvider> {
+ /// Notifier for managing selected clear data types
+ SelectedClearDataTypesProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'selectedClearDataTypesProvider',
+ isAutoDispose: true,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$selectedClearDataTypesHash();
+
+ @$internal
+ @override
+ SelectedClearDataTypes create() => SelectedClearDataTypes();
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(Set value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider>(value),
+ );
+ }
+}
+
+String _$selectedClearDataTypesHash() =>
+ r'f010b5dc724bd214add09f7f85c56dd615003915';
+
+/// Notifier for managing selected clear data types
+
+abstract class _$SelectedClearDataTypes extends $Notifier> {
+ Set build();
+ @$mustCallSuper
+ @override
+ void runBuild() {
+ final ref = this.ref as $Ref, Set>;
+ final element =
+ ref.element
+ as $ClassProviderElement<
+ AnyNotifier, Set>,
+ Set,
+ Object?,
+ Object?
+ >;
+ element.handleCreate(ref, build);
+ }
+}
diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart
index 5c4909b8..c964934f 100644
--- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart
+++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart
@@ -27,6 +27,8 @@ import 'package:weblibre/features/bangs/presentation/widgets/site_search.dart';
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/certificate_tile.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
+import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/clear_site_data_section.dart';
+import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/permissions_section.dart';
import 'package:weblibre/presentation/widgets/website_title_tile.dart';
class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics {
@@ -206,6 +208,17 @@ class ViewTabSheetWidget extends HookConsumerWidget {
),
),
const Divider(),
+ // Permissions Section
+ PermissionsSection(
+ origin: initialTabState.url.origin,
+ isPrivate: initialTabState.isPrivate,
+ ),
+ const Divider(),
+ // Clear Site Data Section
+ ClearSiteDataSection(
+ url: initialTabState.url,
+ ),
+ const SizedBox(height: 16.0),
],
);
},
diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle
index 5ef3da45..6f83df6c 100644
--- a/packages/flutter_mozilla_components/android/build.gradle
+++ b/packages/flutter_mozilla_components/android/build.gradle
@@ -128,6 +128,7 @@ dependencies {
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webauthn:$mozillaComponentsVersion"
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
+ implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PermissionStorage.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PermissionStorage.kt
index c0ade629..982dd54b 100644
--- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PermissionStorage.kt
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PermissionStorage.kt
@@ -64,9 +64,10 @@ class PermissionStorage(
/**
* Deletes all sitePermissions that match the sitePermissions provided as a parameter.
* @param sitePermissions The [SitePermissions] to be deleted from the storage.
+ * @param private Indicates if the [SitePermissions] belongs to a private session.
*/
- suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
- permissionsStorage.remove(sitePermissions, private = false)
+ suspend fun deleteSitePermissions(sitePermissions: SitePermissions, private: Boolean = false) = withContext(dispatcher) {
+ permissionsStorage.remove(sitePermissions, private = private)
}
/**
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt
index bc7613dc..52465b7c 100644
--- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoBrowserApiImpl.kt
@@ -33,6 +33,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoFetchApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoFindApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoHistoryApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoIconsApi
+import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPublicSuffixListApi
+import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSitePermissionsApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoLogging
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoMlApi
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPrefApi
@@ -264,6 +266,8 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
GeckoHistoryApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoHistoryApiImpl())
GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl())
GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl())
+ GeckoSitePermissionsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSitePermissionsApiImpl())
+ GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext))
// Viewport API for dynamic toolbar and keyboard handling
val viewportEvents = GeckoViewportEvents(_flutterPluginBinding.binaryMessenger)
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt
index 45bf3bac..cb2968af 100644
--- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt
@@ -7,6 +7,7 @@
package eu.weblibre.flutter_mozilla_components.api
import eu.weblibre.flutter_mozilla_components.GlobalComponents
+import eu.weblibre.flutter_mozilla_components.pigeons.ClearDataType
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -123,4 +124,40 @@ class GeckoDeleteBrowsingDataControllerImpl : GeckoDeleteBrowsingDataController
}
}
}
+
+ override fun clearDataForHost(
+ host: String,
+ dataTypes: List,
+ callback: (Result) -> Unit
+ ) {
+ coroutineScope.launch {
+ try {
+ withContext(Dispatchers.Main) {
+ // Convert ClearDataType to Engine.BrowsingData flags
+ val browsingDataTypes = dataTypes.map { dataType ->
+ when (dataType) {
+ ClearDataType.AUTH_SESSIONS -> Engine.BrowsingData.AUTH_SESSIONS
+ ClearDataType.ALL_SITE_DATA -> Engine.BrowsingData.ALL_SITE_DATA
+ ClearDataType.COOKIES -> Engine.BrowsingData.COOKIES
+ ClearDataType.ALL_CACHES -> Engine.BrowsingData.ALL_CACHES
+ }
+ }.toIntArray()
+
+ // Clear data for the specific host
+ components.core.engine.clearData(
+ data = Engine.BrowsingData.select(*browsingDataTypes),
+ host = host,
+ onSuccess = {
+ callback(Result.success(Unit))
+ },
+ onError = { throwable ->
+ callback(Result.failure(throwable))
+ }
+ )
+ }
+ } catch (e: Exception) {
+ callback(Result.failure(e))
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPublicSuffixListApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPublicSuffixListApiImpl.kt
new file mode 100644
index 00000000..49935397
--- /dev/null
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPublicSuffixListApiImpl.kt
@@ -0,0 +1,42 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package eu.weblibre.flutter_mozilla_components.api
+
+import android.content.Context
+import eu.weblibre.flutter_mozilla_components.GlobalComponents
+import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPublicSuffixListApi
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+
+class GeckoPublicSuffixListApiImpl(
+ private val context: Context
+) : GeckoPublicSuffixListApi {
+ companion object {
+ private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ }
+
+ private val publicSuffixList by lazy {
+ PublicSuffixList(context)
+ }
+
+ override fun getPublicSuffixPlusOne(host: String, callback: (Result) -> Unit) {
+ coroutineScope.launch {
+ try {
+ // Get the public suffix + 1 (eTLD+1) for the host
+ val result = publicSuffixList.getPublicSuffixPlusOne(host).await()
+ // If result is null, fall back to the original host
+ callback(Result.success(result ?: host))
+ } catch (e: Exception) {
+ // On any error, fall back to the original host
+ callback(Result.success(host))
+ }
+ }
+ }
+}
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoSitePermissionsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoSitePermissionsApiImpl.kt
new file mode 100644
index 00000000..ed37a452
--- /dev/null
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoSitePermissionsApiImpl.kt
@@ -0,0 +1,169 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package eu.weblibre.flutter_mozilla_components.api
+
+import eu.weblibre.flutter_mozilla_components.GlobalComponents
+import eu.weblibre.flutter_mozilla_components.pigeons.AutoplayStatus
+import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSitePermissionsApi
+import eu.weblibre.flutter_mozilla_components.pigeons.SitePermissionStatus
+import eu.weblibre.flutter_mozilla_components.pigeons.SitePermissions as PigeonSitePermissions
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.permission.SitePermissions as MozillaSitePermissions
+import mozilla.components.concept.engine.permission.SitePermissions.Status as MozillaStatus
+import mozilla.components.concept.engine.permission.SitePermissions.AutoplayStatus as MozillaAutoplayStatus
+
+class GeckoSitePermissionsApiImpl : GeckoSitePermissionsApi {
+ companion object {
+ private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ }
+
+ private val components by lazy {
+ requireNotNull(GlobalComponents.components) { "Components not initialized" }
+ }
+
+ override fun getSitePermissions(
+ origin: String,
+ private: Boolean,
+ callback: (Result) -> Unit
+ ) {
+ coroutineScope.launch {
+ try {
+ val permissions = withContext(Dispatchers.IO) {
+ components.core.permissionStorage.findSitePermissionsBy(origin, private)
+ }
+ callback(Result.success(permissions?.toPigeonSitePermissions()))
+ } catch (e: Exception) {
+ callback(Result.failure(e))
+ }
+ }
+ }
+
+ override fun setSitePermissions(
+ permissions: PigeonSitePermissions,
+ private: Boolean,
+ callback: (Result) -> Unit
+ ) {
+ coroutineScope.launch {
+ try {
+ val existingPermissions = withContext(Dispatchers.IO) {
+ components.core.permissionStorage.findSitePermissionsBy(permissions.origin, private)
+ }
+
+ val mozillaPermissions = permissions.toMozillaSitePermissions(existingPermissions)
+
+ withContext(Dispatchers.IO) {
+ if (existingPermissions != null) {
+ components.core.permissionStorage.updateSitePermissions(mozillaPermissions, private)
+ } else {
+ components.core.permissionStorage.add(mozillaPermissions, private)
+ }
+ }
+ callback(Result.success(Unit))
+ } catch (e: Exception) {
+ callback(Result.failure(e))
+ }
+ }
+ }
+
+ override fun deleteSitePermissions(
+ origin: String,
+ private: Boolean,
+ callback: (Result) -> Unit
+ ) {
+ coroutineScope.launch {
+ try {
+ val permissions = withContext(Dispatchers.IO) {
+ components.core.permissionStorage.findSitePermissionsBy(origin, private)
+ }
+ if (permissions != null) {
+ withContext(Dispatchers.IO) {
+ components.core.permissionStorage.deleteSitePermissions(permissions, private)
+ }
+ }
+ callback(Result.success(Unit))
+ } catch (e: Exception) {
+ callback(Result.failure(e))
+ }
+ }
+ }
+}
+
+// Extension function to convert Mozilla SitePermissions to Pigeon format
+private fun MozillaSitePermissions.toPigeonSitePermissions(): PigeonSitePermissions {
+ return PigeonSitePermissions(
+ origin = this.origin,
+ camera = this.camera.toSitePermissionStatus(),
+ microphone = this.microphone.toSitePermissionStatus(),
+ location = this.location.toSitePermissionStatus(),
+ notification = this.notification.toSitePermissionStatus(),
+ persistentStorage = this.localStorage.toSitePermissionStatus(),
+ crossOriginStorageAccess = this.crossOriginStorageAccess.toSitePermissionStatus(),
+ mediaKeySystemAccess = this.mediaKeySystemAccess.toSitePermissionStatus(),
+ localDeviceAccess = null, // Not directly available in Mozilla SitePermissions
+ localNetworkAccess = null, // Not directly available in Mozilla SitePermissions
+ autoplayAudible = this.autoplayAudible.toAutoplayStatus(),
+ autoplayInaudible = this.autoplayInaudible.toAutoplayStatus(),
+ savedAt = this.savedAt,
+ )
+}
+
+// Extension function to convert Pigeon to Mozilla format
+private fun PigeonSitePermissions.toMozillaSitePermissions(
+ existing: MozillaSitePermissions?
+): MozillaSitePermissions {
+ val now = System.currentTimeMillis()
+ return MozillaSitePermissions(
+ origin = this.origin,
+ camera = this.camera?.toMozillaStatus() ?: existing?.camera ?: MozillaStatus.NO_DECISION,
+ microphone = this.microphone?.toMozillaStatus() ?: existing?.microphone ?: MozillaStatus.NO_DECISION,
+ location = this.location?.toMozillaStatus() ?: existing?.location ?: MozillaStatus.NO_DECISION,
+ notification = this.notification?.toMozillaStatus() ?: existing?.notification ?: MozillaStatus.NO_DECISION,
+ localStorage = this.persistentStorage?.toMozillaStatus() ?: existing?.localStorage ?: MozillaStatus.NO_DECISION,
+ crossOriginStorageAccess = this.crossOriginStorageAccess?.toMozillaStatus() ?: existing?.crossOriginStorageAccess ?: MozillaStatus.NO_DECISION,
+ mediaKeySystemAccess = this.mediaKeySystemAccess?.toMozillaStatus() ?: existing?.mediaKeySystemAccess ?: MozillaStatus.NO_DECISION,
+ autoplayAudible = this.autoplayAudible?.toMozillaAutoplayStatus() ?: existing?.autoplayAudible ?: MozillaAutoplayStatus.BLOCKED,
+ autoplayInaudible = this.autoplayInaudible?.toMozillaAutoplayStatus() ?: existing?.autoplayInaudible ?: MozillaAutoplayStatus.ALLOWED,
+ savedAt = if (existing != null) existing.savedAt else now,
+ )
+}
+
+// Helper conversion functions
+private fun MozillaStatus.toSitePermissionStatus(): SitePermissionStatus {
+ return when (this) {
+ MozillaStatus.ALLOWED -> SitePermissionStatus.ALLOWED
+ MozillaStatus.BLOCKED -> SitePermissionStatus.BLOCKED
+ MozillaStatus.NO_DECISION -> SitePermissionStatus.NO_DECISION
+ }
+}
+
+private fun SitePermissionStatus.toMozillaStatus(): MozillaStatus {
+ return when (this) {
+ SitePermissionStatus.ALLOWED -> MozillaStatus.ALLOWED
+ SitePermissionStatus.BLOCKED -> MozillaStatus.BLOCKED
+ SitePermissionStatus.NO_DECISION -> MozillaStatus.NO_DECISION
+ }
+}
+
+private fun MozillaAutoplayStatus.toAutoplayStatus(): AutoplayStatus {
+ return when (this) {
+ MozillaAutoplayStatus.ALLOWED -> AutoplayStatus.ALLOWED
+ MozillaAutoplayStatus.BLOCKED -> AutoplayStatus.BLOCKED
+ }
+}
+
+private fun AutoplayStatus.toMozillaAutoplayStatus(): MozillaAutoplayStatus {
+ return when (this) {
+ AutoplayStatus.ALLOWED -> MozillaAutoplayStatus.ALLOWED
+ AutoplayStatus.BLOCKED -> MozillaAutoplayStatus.BLOCKED
+ AutoplayStatus.BLOCK_AUDIBLE -> MozillaAutoplayStatus.BLOCKED
+ AutoplayStatus.ALLOW_ON_WIFI -> MozillaAutoplayStatus.ALLOWED
+ }
+}
diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt
index 01e47ff0..9b520667 100644
--- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt
+++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt
@@ -412,6 +412,24 @@ enum class MlProgressStatus(val raw: Int) {
}
}
+/** Types of browsing data that can be cleared */
+enum class ClearDataType(val raw: Int) {
+ /** Authentication sessions */
+ AUTH_SESSIONS(0),
+ /** All site data (cookies, storage, etc.) */
+ ALL_SITE_DATA(1),
+ /** Cookies only */
+ COOKIES(2),
+ /** Cache only */
+ ALL_CACHES(3);
+
+ companion object {
+ fun ofRaw(raw: Int): ClearDataType? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
enum class GeckoFetchMethod(val raw: Int) {
GET(0),
HEAD(1),
@@ -463,6 +481,40 @@ enum class BookmarkNodeType(val raw: Int) {
}
}
+/** Permission status for a site permission */
+enum class SitePermissionStatus(val raw: Int) {
+ /** Permission has been granted */
+ ALLOWED(0),
+ /** Permission has been denied */
+ BLOCKED(1),
+ /** No decision has been made yet (ask to allow) */
+ NO_DECISION(2);
+
+ companion object {
+ fun ofRaw(raw: Int): SitePermissionStatus? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
+/** Autoplay permission values (matches Fenix's 4 states) */
+enum class AutoplayStatus(val raw: Int) {
+ /** Allow all autoplay (audible and inaudible) */
+ ALLOWED(0),
+ /** Block all autoplay */
+ BLOCKED(1),
+ /** Block audible autoplay only (allow inaudible) */
+ BLOCK_AUDIBLE(2),
+ /** Allow autoplay on WiFi only */
+ ALLOW_ON_WIFI(3);
+
+ companion object {
+ fun ofRaw(raw: Int): AutoplayStatus? {
+ return values().firstOrNull { it.raw == raw }
+ }
+ }
+}
+
/**
* Translation options that map to the Gecko Translations Options.
*
@@ -2705,6 +2757,74 @@ data class BookmarkInfo (
override fun hashCode(): Int = toList().hashCode()
}
+
+/**
+ * Site permissions data structure
+ *
+ * Generated class from Pigeon that represents data sent in messages.
+ */
+data class SitePermissions (
+ val origin: String,
+ val camera: SitePermissionStatus? = null,
+ val microphone: SitePermissionStatus? = null,
+ val location: SitePermissionStatus? = null,
+ val notification: SitePermissionStatus? = null,
+ val persistentStorage: SitePermissionStatus? = null,
+ val crossOriginStorageAccess: SitePermissionStatus? = null,
+ val mediaKeySystemAccess: SitePermissionStatus? = null,
+ val localDeviceAccess: SitePermissionStatus? = null,
+ val localNetworkAccess: SitePermissionStatus? = null,
+ val autoplayAudible: AutoplayStatus? = null,
+ val autoplayInaudible: AutoplayStatus? = null,
+ val savedAt: Long
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): SitePermissions {
+ val origin = pigeonVar_list[0] as String
+ val camera = pigeonVar_list[1] as SitePermissionStatus?
+ val microphone = pigeonVar_list[2] as SitePermissionStatus?
+ val location = pigeonVar_list[3] as SitePermissionStatus?
+ val notification = pigeonVar_list[4] as SitePermissionStatus?
+ val persistentStorage = pigeonVar_list[5] as SitePermissionStatus?
+ val crossOriginStorageAccess = pigeonVar_list[6] as SitePermissionStatus?
+ val mediaKeySystemAccess = pigeonVar_list[7] as SitePermissionStatus?
+ val localDeviceAccess = pigeonVar_list[8] as SitePermissionStatus?
+ val localNetworkAccess = pigeonVar_list[9] as SitePermissionStatus?
+ val autoplayAudible = pigeonVar_list[10] as AutoplayStatus?
+ val autoplayInaudible = pigeonVar_list[11] as AutoplayStatus?
+ val savedAt = pigeonVar_list[12] as Long
+ return SitePermissions(origin, camera, microphone, location, notification, persistentStorage, crossOriginStorageAccess, mediaKeySystemAccess, localDeviceAccess, localNetworkAccess, autoplayAudible, autoplayInaudible, savedAt)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ origin,
+ camera,
+ microphone,
+ location,
+ notification,
+ persistentStorage,
+ crossOriginStorageAccess,
+ mediaKeySystemAccess,
+ localDeviceAccess,
+ localNetworkAccess,
+ autoplayAudible,
+ autoplayInaudible,
+ savedAt,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other !is SitePermissions) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return GeckoPigeonUtils.deepEquals(toList(), other.toList()) }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
private open class GeckoPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -2815,284 +2935,304 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
}
150.toByte() -> {
return (readValue(buffer) as Long?)?.let {
- GeckoFetchMethod.ofRaw(it.toInt())
+ ClearDataType.ofRaw(it.toInt())
}
}
151.toByte() -> {
return (readValue(buffer) as Long?)?.let {
- GeckoFetchRedircet.ofRaw(it.toInt())
+ GeckoFetchMethod.ofRaw(it.toInt())
}
}
152.toByte() -> {
return (readValue(buffer) as Long?)?.let {
- GeckoFetchCookiePolicy.ofRaw(it.toInt())
+ GeckoFetchRedircet.ofRaw(it.toInt())
}
}
153.toByte() -> {
return (readValue(buffer) as Long?)?.let {
- BookmarkNodeType.ofRaw(it.toInt())
+ GeckoFetchCookiePolicy.ofRaw(it.toInt())
}
}
154.toByte() -> {
- return (readValue(buffer) as? List)?.let {
- TranslationOptions.fromList(it)
+ return (readValue(buffer) as Long?)?.let {
+ BookmarkNodeType.ofRaw(it.toInt())
}
}
155.toByte() -> {
- return (readValue(buffer) as? List)?.let {
- ReaderState.fromList(it)
+ return (readValue(buffer) as Long?)?.let {
+ SitePermissionStatus.ofRaw(it.toInt())
}
}
156.toByte() -> {
- return (readValue(buffer) as? List)?.let {
- AddTabParams.fromList(it)
+ return (readValue(buffer) as Long?)?.let {
+ AutoplayStatus.ofRaw(it.toInt())
}
}
157.toByte() -> {
return (readValue(buffer) as? List)?.let {
- LastMediaAccessState.fromList(it)
+ TranslationOptions.fromList(it)
}
}
158.toByte() -> {
return (readValue(buffer) as? List)?.let {
- HistoryMetadataKey.fromList(it)
+ ReaderState.fromList(it)
}
}
159.toByte() -> {
return (readValue(buffer) as? List)?.let {
- PackageCategoryValue.fromList(it)
+ AddTabParams.fromList(it)
}
}
160.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ExternalPackage.fromList(it)
+ LastMediaAccessState.fromList(it)
}
}
161.toByte() -> {
return (readValue(buffer) as? List)?.let {
- LoadUrlFlagsValue.fromList(it)
+ HistoryMetadataKey.fromList(it)
}
}
162.toByte() -> {
return (readValue(buffer) as? List)?.let {
- SourceValue.fromList(it)
+ PackageCategoryValue.fromList(it)
}
}
163.toByte() -> {
return (readValue(buffer) as? List)?.let {
- TabState.fromList(it)
+ ExternalPackage.fromList(it)
}
}
164.toByte() -> {
return (readValue(buffer) as? List)?.let {
- RecoverableTab.fromList(it)
+ LoadUrlFlagsValue.fromList(it)
}
}
165.toByte() -> {
return (readValue(buffer) as? List)?.let {
- RecoverableBrowserState.fromList(it)
+ SourceValue.fromList(it)
}
}
166.toByte() -> {
return (readValue(buffer) as? List)?.let {
- IconRequest.fromList(it)
+ TabState.fromList(it)
}
}
167.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ResourceSize.fromList(it)
+ RecoverableTab.fromList(it)
}
}
168.toByte() -> {
return (readValue(buffer) as? List)?.let {
- Resource.fromList(it)
+ RecoverableBrowserState.fromList(it)
}
}
169.toByte() -> {
return (readValue(buffer) as? List)?.let {
- IconResult.fromList(it)
+ IconRequest.fromList(it)
}
}
170.toByte() -> {
return (readValue(buffer) as? List)?.let {
- CookiePartitionKey.fromList(it)
+ ResourceSize.fromList(it)
}
}
171.toByte() -> {
return (readValue(buffer) as? List)?.let {
- Cookie.fromList(it)
+ Resource.fromList(it)
}
}
172.toByte() -> {
return (readValue(buffer) as? List)?.let {
- VisitInfo.fromList(it)
+ IconResult.fromList(it)
}
}
173.toByte() -> {
return (readValue(buffer) as? List)?.let {
- HistoryItem.fromList(it)
+ CookiePartitionKey.fromList(it)
}
}
174.toByte() -> {
return (readValue(buffer) as? List)?.let {
- HistoryState.fromList(it)
+ Cookie.fromList(it)
}
}
175.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ReaderableState.fromList(it)
+ VisitInfo.fromList(it)
}
}
176.toByte() -> {
return (readValue(buffer) as? List)?.let {
- SecurityInfoState.fromList(it)
+ HistoryItem.fromList(it)
}
}
177.toByte() -> {
return (readValue(buffer) as? List)?.let {
- TabContentState.fromList(it)
+ HistoryState.fromList(it)
}
}
178.toByte() -> {
return (readValue(buffer) as? List)?.let {
- FindResultState.fromList(it)
+ ReaderableState.fromList(it)
}
}
179.toByte() -> {
return (readValue(buffer) as? List)?.let {
- CustomSelectionAction.fromList(it)
+ SecurityInfoState.fromList(it)
}
}
180.toByte() -> {
return (readValue(buffer) as? List)?.let {
- WebExtensionData.fromList(it)
+ TabContentState.fromList(it)
}
}
181.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoSuggestion.fromList(it)
+ FindResultState.fromList(it)
}
}
182.toByte() -> {
return (readValue(buffer) as? List)?.let {
- TabContent.fromList(it)
+ CustomSelectionAction.fromList(it)
}
}
183.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ContentBlocking.fromList(it)
+ WebExtensionData.fromList(it)
}
}
184.toByte() -> {
return (readValue(buffer) as? List)?.let {
- DohSettings.fromList(it)
+ GeckoSuggestion.fromList(it)
}
}
185.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoEngineSettings.fromList(it)
+ TabContent.fromList(it)
}
}
186.toByte() -> {
return (readValue(buffer) as? List)?.let {
- AutocompleteResult.fromList(it)
+ ContentBlocking.fromList(it)
}
}
187.toByte() -> {
return (readValue(buffer) as? List)?.let {
- UnknownHitResult.fromList(it)
+ DohSettings.fromList(it)
}
}
188.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ImageHitResult.fromList(it)
+ GeckoEngineSettings.fromList(it)
}
}
189.toByte() -> {
return (readValue(buffer) as? List)?.let {
- VideoHitResult.fromList(it)
+ AutocompleteResult.fromList(it)
}
}
190.toByte() -> {
return (readValue(buffer) as? List)?.let {
- AudioHitResult.fromList(it)
+ UnknownHitResult.fromList(it)
}
}
191.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ImageSrcHitResult.fromList(it)
+ ImageHitResult.fromList(it)
}
}
192.toByte() -> {
return (readValue(buffer) as? List)?.let {
- PhoneHitResult.fromList(it)
+ VideoHitResult.fromList(it)
}
}
193.toByte() -> {
return (readValue(buffer) as? List)?.let {
- EmailHitResult.fromList(it)
+ AudioHitResult.fromList(it)
}
}
194.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeoHitResult.fromList(it)
+ ImageSrcHitResult.fromList(it)
}
}
195.toByte() -> {
return (readValue(buffer) as? List)?.let {
- DownloadState.fromList(it)
+ PhoneHitResult.fromList(it)
}
}
196.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ShareInternetResourceState.fromList(it)
+ EmailHitResult.fromList(it)
}
}
197.toByte() -> {
return (readValue(buffer) as? List)?.let {
- AddonCollection.fromList(it)
+ GeoHitResult.fromList(it)
}
}
198.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoPref.fromList(it)
+ DownloadState.fromList(it)
}
}
199.toByte() -> {
return (readValue(buffer) as? List)?.let {
- MlProgressData.fromList(it)
+ ShareInternetResourceState.fromList(it)
}
}
200.toByte() -> {
return (readValue(buffer) as? List)?.let {
- ContainerSiteAssignment.fromList(it)
+ AddonCollection.fromList(it)
}
}
201.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoHeader.fromList(it)
+ GeckoPref.fromList(it)
}
}
202.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoFetchRequest.fromList(it)
+ MlProgressData.fromList(it)
}
}
203.toByte() -> {
return (readValue(buffer) as? List)?.let {
- GeckoFetchResponse.fromList(it)
+ ContainerSiteAssignment.fromList(it)
}
}
204.toByte() -> {
return (readValue(buffer) as? List)?.let {
- BookmarkNode.fromList(it)
+ GeckoHeader.fromList(it)
}
}
205.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ GeckoFetchRequest.fromList(it)
+ }
+ }
+ 206.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ GeckoFetchResponse.fromList(it)
+ }
+ }
+ 207.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ BookmarkNode.fromList(it)
+ }
+ }
+ 208.toByte() -> {
return (readValue(buffer) as? List)?.let {
BookmarkInfo.fromList(it)
}
}
+ 209.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ SitePermissions.fromList(it)
+ }
+ }
else -> super.readValueOfType(type, buffer)
}
}
@@ -3182,230 +3322,246 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
stream.write(149)
writeValue(stream, value.raw.toLong())
}
- is GeckoFetchMethod -> {
+ is ClearDataType -> {
stream.write(150)
writeValue(stream, value.raw.toLong())
}
- is GeckoFetchRedircet -> {
+ is GeckoFetchMethod -> {
stream.write(151)
writeValue(stream, value.raw.toLong())
}
- is GeckoFetchCookiePolicy -> {
+ is GeckoFetchRedircet -> {
stream.write(152)
writeValue(stream, value.raw.toLong())
}
- is BookmarkNodeType -> {
+ is GeckoFetchCookiePolicy -> {
stream.write(153)
writeValue(stream, value.raw.toLong())
}
- is TranslationOptions -> {
+ is BookmarkNodeType -> {
stream.write(154)
- writeValue(stream, value.toList())
+ writeValue(stream, value.raw.toLong())
}
- is ReaderState -> {
+ is SitePermissionStatus -> {
stream.write(155)
- writeValue(stream, value.toList())
+ writeValue(stream, value.raw.toLong())
}
- is AddTabParams -> {
+ is AutoplayStatus -> {
stream.write(156)
- writeValue(stream, value.toList())
+ writeValue(stream, value.raw.toLong())
}
- is LastMediaAccessState -> {
+ is TranslationOptions -> {
stream.write(157)
writeValue(stream, value.toList())
}
- is HistoryMetadataKey -> {
+ is ReaderState -> {
stream.write(158)
writeValue(stream, value.toList())
}
- is PackageCategoryValue -> {
+ is AddTabParams -> {
stream.write(159)
writeValue(stream, value.toList())
}
- is ExternalPackage -> {
+ is LastMediaAccessState -> {
stream.write(160)
writeValue(stream, value.toList())
}
- is LoadUrlFlagsValue -> {
+ is HistoryMetadataKey -> {
stream.write(161)
writeValue(stream, value.toList())
}
- is SourceValue -> {
+ is PackageCategoryValue -> {
stream.write(162)
writeValue(stream, value.toList())
}
- is TabState -> {
+ is ExternalPackage -> {
stream.write(163)
writeValue(stream, value.toList())
}
- is RecoverableTab -> {
+ is LoadUrlFlagsValue -> {
stream.write(164)
writeValue(stream, value.toList())
}
- is RecoverableBrowserState -> {
+ is SourceValue -> {
stream.write(165)
writeValue(stream, value.toList())
}
- is IconRequest -> {
+ is TabState -> {
stream.write(166)
writeValue(stream, value.toList())
}
- is ResourceSize -> {
+ is RecoverableTab -> {
stream.write(167)
writeValue(stream, value.toList())
}
- is Resource -> {
+ is RecoverableBrowserState -> {
stream.write(168)
writeValue(stream, value.toList())
}
- is IconResult -> {
+ is IconRequest -> {
stream.write(169)
writeValue(stream, value.toList())
}
- is CookiePartitionKey -> {
+ is ResourceSize -> {
stream.write(170)
writeValue(stream, value.toList())
}
- is Cookie -> {
+ is Resource -> {
stream.write(171)
writeValue(stream, value.toList())
}
- is VisitInfo -> {
+ is IconResult -> {
stream.write(172)
writeValue(stream, value.toList())
}
- is HistoryItem -> {
+ is CookiePartitionKey -> {
stream.write(173)
writeValue(stream, value.toList())
}
- is HistoryState -> {
+ is Cookie -> {
stream.write(174)
writeValue(stream, value.toList())
}
- is ReaderableState -> {
+ is VisitInfo -> {
stream.write(175)
writeValue(stream, value.toList())
}
- is SecurityInfoState -> {
+ is HistoryItem -> {
stream.write(176)
writeValue(stream, value.toList())
}
- is TabContentState -> {
+ is HistoryState -> {
stream.write(177)
writeValue(stream, value.toList())
}
- is FindResultState -> {
+ is ReaderableState -> {
stream.write(178)
writeValue(stream, value.toList())
}
- is CustomSelectionAction -> {
+ is SecurityInfoState -> {
stream.write(179)
writeValue(stream, value.toList())
}
- is WebExtensionData -> {
+ is TabContentState -> {
stream.write(180)
writeValue(stream, value.toList())
}
- is GeckoSuggestion -> {
+ is FindResultState -> {
stream.write(181)
writeValue(stream, value.toList())
}
- is TabContent -> {
+ is CustomSelectionAction -> {
stream.write(182)
writeValue(stream, value.toList())
}
- is ContentBlocking -> {
+ is WebExtensionData -> {
stream.write(183)
writeValue(stream, value.toList())
}
- is DohSettings -> {
+ is GeckoSuggestion -> {
stream.write(184)
writeValue(stream, value.toList())
}
- is GeckoEngineSettings -> {
+ is TabContent -> {
stream.write(185)
writeValue(stream, value.toList())
}
- is AutocompleteResult -> {
+ is ContentBlocking -> {
stream.write(186)
writeValue(stream, value.toList())
}
- is UnknownHitResult -> {
+ is DohSettings -> {
stream.write(187)
writeValue(stream, value.toList())
}
- is ImageHitResult -> {
+ is GeckoEngineSettings -> {
stream.write(188)
writeValue(stream, value.toList())
}
- is VideoHitResult -> {
+ is AutocompleteResult -> {
stream.write(189)
writeValue(stream, value.toList())
}
- is AudioHitResult -> {
+ is UnknownHitResult -> {
stream.write(190)
writeValue(stream, value.toList())
}
- is ImageSrcHitResult -> {
+ is ImageHitResult -> {
stream.write(191)
writeValue(stream, value.toList())
}
- is PhoneHitResult -> {
+ is VideoHitResult -> {
stream.write(192)
writeValue(stream, value.toList())
}
- is EmailHitResult -> {
+ is AudioHitResult -> {
stream.write(193)
writeValue(stream, value.toList())
}
- is GeoHitResult -> {
+ is ImageSrcHitResult -> {
stream.write(194)
writeValue(stream, value.toList())
}
- is DownloadState -> {
+ is PhoneHitResult -> {
stream.write(195)
writeValue(stream, value.toList())
}
- is ShareInternetResourceState -> {
+ is EmailHitResult -> {
stream.write(196)
writeValue(stream, value.toList())
}
- is AddonCollection -> {
+ is GeoHitResult -> {
stream.write(197)
writeValue(stream, value.toList())
}
- is GeckoPref -> {
+ is DownloadState -> {
stream.write(198)
writeValue(stream, value.toList())
}
- is MlProgressData -> {
+ is ShareInternetResourceState -> {
stream.write(199)
writeValue(stream, value.toList())
}
- is ContainerSiteAssignment -> {
+ is AddonCollection -> {
stream.write(200)
writeValue(stream, value.toList())
}
- is GeckoHeader -> {
+ is GeckoPref -> {
stream.write(201)
writeValue(stream, value.toList())
}
- is GeckoFetchRequest -> {
+ is MlProgressData -> {
stream.write(202)
writeValue(stream, value.toList())
}
- is GeckoFetchResponse -> {
+ is ContainerSiteAssignment -> {
stream.write(203)
writeValue(stream, value.toList())
}
- is BookmarkNode -> {
+ is GeckoHeader -> {
stream.write(204)
writeValue(stream, value.toList())
}
- is BookmarkInfo -> {
+ is GeckoFetchRequest -> {
stream.write(205)
writeValue(stream, value.toList())
}
+ is GeckoFetchResponse -> {
+ stream.write(206)
+ writeValue(stream, value.toList())
+ }
+ is BookmarkNode -> {
+ stream.write(207)
+ writeValue(stream, value.toList())
+ }
+ is BookmarkInfo -> {
+ stream.write(208)
+ writeValue(stream, value.toList())
+ }
+ is SitePermissions -> {
+ stream.write(209)
+ writeValue(stream, value.toList())
+ }
else -> super.writeValue(stream, value)
}
}
@@ -5601,6 +5757,8 @@ interface GeckoDeleteBrowsingDataController {
fun deleteSitePermissions(callback: (Result) -> Unit)
fun deleteDownloads(callback: (Result) -> Unit)
fun clearDataForSessionContext(contextId: String, callback: (Result) -> Unit)
+ /** Clear browsing data for a specific host/domain */
+ fun clearDataForHost(host: String, dataTypes: List, callback: (Result) -> Unit)
companion object {
/** The codec used by GeckoDeleteBrowsingDataController. */
@@ -5732,6 +5890,26 @@ interface GeckoDeleteBrowsingDataController {
channel.setMessageHandler(null)
}
}
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForHost$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val hostArg = args[0] as String
+ val dataTypesArg = args[1] as List
+ api.clearDataForHost(hostArg, dataTypesArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(GeckoPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(GeckoPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
}
}
}
@@ -6409,3 +6587,133 @@ interface GeckoBookmarksApi {
}
}
}
+/**
+ * API for managing site permissions stored in GeckoView
+ *
+ * Generated interface from Pigeon that represents a handler of messages from Flutter.
+ */
+interface GeckoSitePermissionsApi {
+ /** Get permissions for origin (single source of truth from GeckoView) */
+ fun getSitePermissions(origin: String, private: Boolean, callback: (Result) -> Unit)
+ /** Save/update permissions (persisted by GeckoView) */
+ fun setSitePermissions(permissions: SitePermissions, private: Boolean, callback: (Result) -> Unit)
+ /** Delete permissions for origin (removed from GeckoView storage) */
+ fun deleteSitePermissions(origin: String, private: Boolean, callback: (Result) -> Unit)
+
+ companion object {
+ /** The codec used by GeckoSitePermissionsApi. */
+ val codec: MessageCodec by lazy {
+ GeckoPigeonCodec()
+ }
+ /** Sets up an instance of `GeckoSitePermissionsApi` to handle messages through the `binaryMessenger`. */
+ @JvmOverloads
+ fun setUp(binaryMessenger: BinaryMessenger, api: GeckoSitePermissionsApi?, messageChannelSuffix: String = "") {
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.getSitePermissions$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val originArg = args[0] as String
+ val privateArg = args[1] as Boolean
+ api.getSitePermissions(originArg, privateArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(GeckoPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(GeckoPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.setSitePermissions$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val permissionsArg = args[0] as SitePermissions
+ val privateArg = args[1] as Boolean
+ api.setSitePermissions(permissionsArg, privateArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(GeckoPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(GeckoPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.deleteSitePermissions$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val originArg = args[0] as String
+ val privateArg = args[1] as Boolean
+ api.deleteSitePermissions(originArg, privateArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(GeckoPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(GeckoPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
+/**
+ * Native wrapper for Mozilla's Public Suffix List
+ *
+ * Generated interface from Pigeon that represents a handler of messages from Flutter.
+ */
+interface GeckoPublicSuffixListApi {
+ /**
+ * Get base domain (eTLD+1) from host using Mozilla's Public Suffix List
+ * Returns the host unchanged if PSL lookup fails
+ */
+ fun getPublicSuffixPlusOne(host: String, callback: (Result) -> Unit)
+
+ companion object {
+ /** The codec used by GeckoPublicSuffixListApi. */
+ val codec: MessageCodec by lazy {
+ GeckoPigeonCodec()
+ }
+ /** Sets up an instance of `GeckoPublicSuffixListApi` to handle messages through the `binaryMessenger`. */
+ @JvmOverloads
+ fun setUp(binaryMessenger: BinaryMessenger, api: GeckoPublicSuffixListApi?, messageChannelSuffix: String = "") {
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoPublicSuffixListApi.getPublicSuffixPlusOne$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val hostArg = args[0] as String
+ api.getPublicSuffixPlusOne(hostArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(GeckoPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(GeckoPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart
index 2698a849..cec71388 100644
--- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart
+++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart
@@ -37,10 +37,12 @@ export 'src/pigeons/gecko.g.dart'
AddTabParams,
AddonCollection,
AudioHitResult,
+ AutoplayStatus,
BookmarkInfo,
BookmarkNode,
BookmarkNodeType,
BounceTrackingProtectionMode,
+ ClearDataType,
ColorScheme,
ContentBlocking,
CookieBannerHandlingMode,
@@ -48,9 +50,12 @@ export 'src/pigeons/gecko.g.dart'
DohSettings,
DohSettingsMode,
EmailHitResult,
+ GeckoDeleteBrowsingDataController,
GeckoEngineSettings,
GeckoFetchResponse,
GeckoPref,
+ GeckoPublicSuffixListApi,
+ GeckoSitePermissionsApi,
GeckoSuggestion,
GeckoSuggestionType,
GeoHitResult,
@@ -70,6 +75,8 @@ export 'src/pigeons/gecko.g.dart'
Resource,
ResourceSize,
SecurityInfoState,
+ SitePermissions,
+ SitePermissionStatus,
TabContent,
TabContentState,
TrackingProtectionPolicy,
diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart
index 9d0d6e3a..d077e6f8 100644
--- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart
+++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart
@@ -232,6 +232,18 @@ enum MlProgressStatus {
done,
}
+/// Types of browsing data that can be cleared
+enum ClearDataType {
+ /// Authentication sessions
+ authSessions,
+ /// All site data (cookies, storage, etc.)
+ allSiteData,
+ /// Cookies only
+ cookies,
+ /// Cache only
+ allCaches,
+}
+
enum GeckoFetchMethod {
get,
head,
@@ -259,6 +271,28 @@ enum BookmarkNodeType {
separator,
}
+/// Permission status for a site permission
+enum SitePermissionStatus {
+ /// Permission has been granted
+ allowed,
+ /// Permission has been denied
+ blocked,
+ /// No decision has been made yet (ask to allow)
+ noDecision,
+}
+
+/// Autoplay permission values (matches Fenix's 4 states)
+enum AutoplayStatus {
+ /// Allow all autoplay (audible and inaudible)
+ allowed,
+ /// Block all autoplay
+ blocked,
+ /// Block audible autoplay only (allow inaudible)
+ blockAudible,
+ /// Allow autoplay on WiFi only
+ allowOnWifi,
+}
+
/// Translation options that map to the Gecko Translations Options.
///
/// @property downloadModel If the necessary models should be downloaded on request. If false, then
@@ -3477,6 +3511,108 @@ class BookmarkInfo {
;
}
+/// Site permissions data structure
+class SitePermissions {
+ SitePermissions({
+ required this.origin,
+ this.camera,
+ this.microphone,
+ this.location,
+ this.notification,
+ this.persistentStorage,
+ this.crossOriginStorageAccess,
+ this.mediaKeySystemAccess,
+ this.localDeviceAccess,
+ this.localNetworkAccess,
+ this.autoplayAudible,
+ this.autoplayInaudible,
+ required this.savedAt,
+ });
+
+ String origin;
+
+ SitePermissionStatus? camera;
+
+ SitePermissionStatus? microphone;
+
+ SitePermissionStatus? location;
+
+ SitePermissionStatus? notification;
+
+ SitePermissionStatus? persistentStorage;
+
+ SitePermissionStatus? crossOriginStorageAccess;
+
+ SitePermissionStatus? mediaKeySystemAccess;
+
+ SitePermissionStatus? localDeviceAccess;
+
+ SitePermissionStatus? localNetworkAccess;
+
+ AutoplayStatus? autoplayAudible;
+
+ AutoplayStatus? autoplayInaudible;
+
+ int savedAt;
+
+ List