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 _toList() { + return [ + origin, + camera, + microphone, + location, + notification, + persistentStorage, + crossOriginStorageAccess, + mediaKeySystemAccess, + localDeviceAccess, + localNetworkAccess, + autoplayAudible, + autoplayInaudible, + savedAt, + ]; + } + + Object encode() { + return _toList(); } + + static SitePermissions decode(Object result) { + result as List; + return SitePermissions( + origin: result[0]! as String, + camera: result[1] as SitePermissionStatus?, + microphone: result[2] as SitePermissionStatus?, + location: result[3] as SitePermissionStatus?, + notification: result[4] as SitePermissionStatus?, + persistentStorage: result[5] as SitePermissionStatus?, + crossOriginStorageAccess: result[6] as SitePermissionStatus?, + mediaKeySystemAccess: result[7] as SitePermissionStatus?, + localDeviceAccess: result[8] as SitePermissionStatus?, + localNetworkAccess: result[9] as SitePermissionStatus?, + autoplayAudible: result[10] as AutoplayStatus?, + autoplayInaudible: result[11] as AutoplayStatus?, + savedAt: result[12]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SitePermissions || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()) +; +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @@ -3548,174 +3684,186 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is MlProgressStatus) { buffer.putUint8(149); writeValue(buffer, value.index); - } else if (value is GeckoFetchMethod) { + } else if (value is ClearDataType) { buffer.putUint8(150); writeValue(buffer, value.index); - } else if (value is GeckoFetchRedircet) { + } else if (value is GeckoFetchMethod) { buffer.putUint8(151); writeValue(buffer, value.index); - } else if (value is GeckoFetchCookiePolicy) { + } else if (value is GeckoFetchRedircet) { buffer.putUint8(152); writeValue(buffer, value.index); - } else if (value is BookmarkNodeType) { + } else if (value is GeckoFetchCookiePolicy) { buffer.putUint8(153); writeValue(buffer, value.index); - } else if (value is TranslationOptions) { + } else if (value is BookmarkNodeType) { buffer.putUint8(154); - writeValue(buffer, value.encode()); - } else if (value is ReaderState) { + writeValue(buffer, value.index); + } else if (value is SitePermissionStatus) { buffer.putUint8(155); - writeValue(buffer, value.encode()); - } else if (value is AddTabParams) { + writeValue(buffer, value.index); + } else if (value is AutoplayStatus) { buffer.putUint8(156); - writeValue(buffer, value.encode()); - } else if (value is LastMediaAccessState) { + writeValue(buffer, value.index); + } else if (value is TranslationOptions) { buffer.putUint8(157); writeValue(buffer, value.encode()); - } else if (value is HistoryMetadataKey) { + } else if (value is ReaderState) { buffer.putUint8(158); writeValue(buffer, value.encode()); - } else if (value is PackageCategoryValue) { + } else if (value is AddTabParams) { buffer.putUint8(159); writeValue(buffer, value.encode()); - } else if (value is ExternalPackage) { + } else if (value is LastMediaAccessState) { buffer.putUint8(160); writeValue(buffer, value.encode()); - } else if (value is LoadUrlFlagsValue) { + } else if (value is HistoryMetadataKey) { buffer.putUint8(161); writeValue(buffer, value.encode()); - } else if (value is SourceValue) { + } else if (value is PackageCategoryValue) { buffer.putUint8(162); writeValue(buffer, value.encode()); - } else if (value is TabState) { + } else if (value is ExternalPackage) { buffer.putUint8(163); writeValue(buffer, value.encode()); - } else if (value is RecoverableTab) { + } else if (value is LoadUrlFlagsValue) { buffer.putUint8(164); writeValue(buffer, value.encode()); - } else if (value is RecoverableBrowserState) { + } else if (value is SourceValue) { buffer.putUint8(165); writeValue(buffer, value.encode()); - } else if (value is IconRequest) { + } else if (value is TabState) { buffer.putUint8(166); writeValue(buffer, value.encode()); - } else if (value is ResourceSize) { + } else if (value is RecoverableTab) { buffer.putUint8(167); writeValue(buffer, value.encode()); - } else if (value is Resource) { + } else if (value is RecoverableBrowserState) { buffer.putUint8(168); writeValue(buffer, value.encode()); - } else if (value is IconResult) { + } else if (value is IconRequest) { buffer.putUint8(169); writeValue(buffer, value.encode()); - } else if (value is CookiePartitionKey) { + } else if (value is ResourceSize) { buffer.putUint8(170); writeValue(buffer, value.encode()); - } else if (value is Cookie) { + } else if (value is Resource) { buffer.putUint8(171); writeValue(buffer, value.encode()); - } else if (value is VisitInfo) { + } else if (value is IconResult) { buffer.putUint8(172); writeValue(buffer, value.encode()); - } else if (value is HistoryItem) { + } else if (value is CookiePartitionKey) { buffer.putUint8(173); writeValue(buffer, value.encode()); - } else if (value is HistoryState) { + } else if (value is Cookie) { buffer.putUint8(174); writeValue(buffer, value.encode()); - } else if (value is ReaderableState) { + } else if (value is VisitInfo) { buffer.putUint8(175); writeValue(buffer, value.encode()); - } else if (value is SecurityInfoState) { + } else if (value is HistoryItem) { buffer.putUint8(176); writeValue(buffer, value.encode()); - } else if (value is TabContentState) { + } else if (value is HistoryState) { buffer.putUint8(177); writeValue(buffer, value.encode()); - } else if (value is FindResultState) { + } else if (value is ReaderableState) { buffer.putUint8(178); writeValue(buffer, value.encode()); - } else if (value is CustomSelectionAction) { + } else if (value is SecurityInfoState) { buffer.putUint8(179); writeValue(buffer, value.encode()); - } else if (value is WebExtensionData) { + } else if (value is TabContentState) { buffer.putUint8(180); writeValue(buffer, value.encode()); - } else if (value is GeckoSuggestion) { + } else if (value is FindResultState) { buffer.putUint8(181); writeValue(buffer, value.encode()); - } else if (value is TabContent) { + } else if (value is CustomSelectionAction) { buffer.putUint8(182); writeValue(buffer, value.encode()); - } else if (value is ContentBlocking) { + } else if (value is WebExtensionData) { buffer.putUint8(183); writeValue(buffer, value.encode()); - } else if (value is DohSettings) { + } else if (value is GeckoSuggestion) { buffer.putUint8(184); writeValue(buffer, value.encode()); - } else if (value is GeckoEngineSettings) { + } else if (value is TabContent) { buffer.putUint8(185); writeValue(buffer, value.encode()); - } else if (value is AutocompleteResult) { + } else if (value is ContentBlocking) { buffer.putUint8(186); writeValue(buffer, value.encode()); - } else if (value is UnknownHitResult) { + } else if (value is DohSettings) { buffer.putUint8(187); writeValue(buffer, value.encode()); - } else if (value is ImageHitResult) { + } else if (value is GeckoEngineSettings) { buffer.putUint8(188); writeValue(buffer, value.encode()); - } else if (value is VideoHitResult) { + } else if (value is AutocompleteResult) { buffer.putUint8(189); writeValue(buffer, value.encode()); - } else if (value is AudioHitResult) { + } else if (value is UnknownHitResult) { buffer.putUint8(190); writeValue(buffer, value.encode()); - } else if (value is ImageSrcHitResult) { + } else if (value is ImageHitResult) { buffer.putUint8(191); writeValue(buffer, value.encode()); - } else if (value is PhoneHitResult) { + } else if (value is VideoHitResult) { buffer.putUint8(192); writeValue(buffer, value.encode()); - } else if (value is EmailHitResult) { + } else if (value is AudioHitResult) { buffer.putUint8(193); writeValue(buffer, value.encode()); - } else if (value is GeoHitResult) { + } else if (value is ImageSrcHitResult) { buffer.putUint8(194); writeValue(buffer, value.encode()); - } else if (value is DownloadState) { + } else if (value is PhoneHitResult) { buffer.putUint8(195); writeValue(buffer, value.encode()); - } else if (value is ShareInternetResourceState) { + } else if (value is EmailHitResult) { buffer.putUint8(196); writeValue(buffer, value.encode()); - } else if (value is AddonCollection) { + } else if (value is GeoHitResult) { buffer.putUint8(197); writeValue(buffer, value.encode()); - } else if (value is GeckoPref) { + } else if (value is DownloadState) { buffer.putUint8(198); writeValue(buffer, value.encode()); - } else if (value is MlProgressData) { + } else if (value is ShareInternetResourceState) { buffer.putUint8(199); writeValue(buffer, value.encode()); - } else if (value is ContainerSiteAssignment) { + } else if (value is AddonCollection) { buffer.putUint8(200); writeValue(buffer, value.encode()); - } else if (value is GeckoHeader) { + } else if (value is GeckoPref) { buffer.putUint8(201); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchRequest) { + } else if (value is MlProgressData) { buffer.putUint8(202); writeValue(buffer, value.encode()); - } else if (value is GeckoFetchResponse) { + } else if (value is ContainerSiteAssignment) { buffer.putUint8(203); writeValue(buffer, value.encode()); - } else if (value is BookmarkNode) { + } else if (value is GeckoHeader) { buffer.putUint8(204); writeValue(buffer, value.encode()); - } else if (value is BookmarkInfo) { + } else if (value is GeckoFetchRequest) { buffer.putUint8(205); writeValue(buffer, value.encode()); + } else if (value is GeckoFetchResponse) { + buffer.putUint8(206); + writeValue(buffer, value.encode()); + } else if (value is BookmarkNode) { + buffer.putUint8(207); + writeValue(buffer, value.encode()); + } else if (value is BookmarkInfo) { + buffer.putUint8(208); + writeValue(buffer, value.encode()); + } else if (value is SitePermissions) { + buffer.putUint8(209); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -3789,120 +3937,131 @@ class _PigeonCodec extends StandardMessageCodec { return value == null ? null : MlProgressStatus.values[value]; case 150: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchMethod.values[value]; + return value == null ? null : ClearDataType.values[value]; case 151: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchRedircet.values[value]; + return value == null ? null : GeckoFetchMethod.values[value]; case 152: final value = readValue(buffer) as int?; - return value == null ? null : GeckoFetchCookiePolicy.values[value]; + return value == null ? null : GeckoFetchRedircet.values[value]; case 153: final value = readValue(buffer) as int?; - return value == null ? null : BookmarkNodeType.values[value]; + return value == null ? null : GeckoFetchCookiePolicy.values[value]; case 154: - return TranslationOptions.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : BookmarkNodeType.values[value]; case 155: - return ReaderState.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : SitePermissionStatus.values[value]; case 156: - return AddTabParams.decode(readValue(buffer)!); + final value = readValue(buffer) as int?; + return value == null ? null : AutoplayStatus.values[value]; case 157: - return LastMediaAccessState.decode(readValue(buffer)!); + return TranslationOptions.decode(readValue(buffer)!); case 158: - return HistoryMetadataKey.decode(readValue(buffer)!); + return ReaderState.decode(readValue(buffer)!); case 159: - return PackageCategoryValue.decode(readValue(buffer)!); + return AddTabParams.decode(readValue(buffer)!); case 160: - return ExternalPackage.decode(readValue(buffer)!); + return LastMediaAccessState.decode(readValue(buffer)!); case 161: - return LoadUrlFlagsValue.decode(readValue(buffer)!); + return HistoryMetadataKey.decode(readValue(buffer)!); case 162: - return SourceValue.decode(readValue(buffer)!); + return PackageCategoryValue.decode(readValue(buffer)!); case 163: - return TabState.decode(readValue(buffer)!); + return ExternalPackage.decode(readValue(buffer)!); case 164: - return RecoverableTab.decode(readValue(buffer)!); + return LoadUrlFlagsValue.decode(readValue(buffer)!); case 165: - return RecoverableBrowserState.decode(readValue(buffer)!); + return SourceValue.decode(readValue(buffer)!); case 166: - return IconRequest.decode(readValue(buffer)!); + return TabState.decode(readValue(buffer)!); case 167: - return ResourceSize.decode(readValue(buffer)!); + return RecoverableTab.decode(readValue(buffer)!); case 168: - return Resource.decode(readValue(buffer)!); + return RecoverableBrowserState.decode(readValue(buffer)!); case 169: - return IconResult.decode(readValue(buffer)!); + return IconRequest.decode(readValue(buffer)!); case 170: - return CookiePartitionKey.decode(readValue(buffer)!); + return ResourceSize.decode(readValue(buffer)!); case 171: - return Cookie.decode(readValue(buffer)!); + return Resource.decode(readValue(buffer)!); case 172: - return VisitInfo.decode(readValue(buffer)!); + return IconResult.decode(readValue(buffer)!); case 173: - return HistoryItem.decode(readValue(buffer)!); + return CookiePartitionKey.decode(readValue(buffer)!); case 174: - return HistoryState.decode(readValue(buffer)!); + return Cookie.decode(readValue(buffer)!); case 175: - return ReaderableState.decode(readValue(buffer)!); + return VisitInfo.decode(readValue(buffer)!); case 176: - return SecurityInfoState.decode(readValue(buffer)!); + return HistoryItem.decode(readValue(buffer)!); case 177: - return TabContentState.decode(readValue(buffer)!); + return HistoryState.decode(readValue(buffer)!); case 178: - return FindResultState.decode(readValue(buffer)!); + return ReaderableState.decode(readValue(buffer)!); case 179: - return CustomSelectionAction.decode(readValue(buffer)!); + return SecurityInfoState.decode(readValue(buffer)!); case 180: - return WebExtensionData.decode(readValue(buffer)!); + return TabContentState.decode(readValue(buffer)!); case 181: - return GeckoSuggestion.decode(readValue(buffer)!); + return FindResultState.decode(readValue(buffer)!); case 182: - return TabContent.decode(readValue(buffer)!); + return CustomSelectionAction.decode(readValue(buffer)!); case 183: - return ContentBlocking.decode(readValue(buffer)!); + return WebExtensionData.decode(readValue(buffer)!); case 184: - return DohSettings.decode(readValue(buffer)!); + return GeckoSuggestion.decode(readValue(buffer)!); case 185: - return GeckoEngineSettings.decode(readValue(buffer)!); + return TabContent.decode(readValue(buffer)!); case 186: - return AutocompleteResult.decode(readValue(buffer)!); + return ContentBlocking.decode(readValue(buffer)!); case 187: - return UnknownHitResult.decode(readValue(buffer)!); + return DohSettings.decode(readValue(buffer)!); case 188: - return ImageHitResult.decode(readValue(buffer)!); + return GeckoEngineSettings.decode(readValue(buffer)!); case 189: - return VideoHitResult.decode(readValue(buffer)!); + return AutocompleteResult.decode(readValue(buffer)!); case 190: - return AudioHitResult.decode(readValue(buffer)!); + return UnknownHitResult.decode(readValue(buffer)!); case 191: - return ImageSrcHitResult.decode(readValue(buffer)!); + return ImageHitResult.decode(readValue(buffer)!); case 192: - return PhoneHitResult.decode(readValue(buffer)!); + return VideoHitResult.decode(readValue(buffer)!); case 193: - return EmailHitResult.decode(readValue(buffer)!); + return AudioHitResult.decode(readValue(buffer)!); case 194: - return GeoHitResult.decode(readValue(buffer)!); + return ImageSrcHitResult.decode(readValue(buffer)!); case 195: - return DownloadState.decode(readValue(buffer)!); + return PhoneHitResult.decode(readValue(buffer)!); case 196: - return ShareInternetResourceState.decode(readValue(buffer)!); + return EmailHitResult.decode(readValue(buffer)!); case 197: - return AddonCollection.decode(readValue(buffer)!); + return GeoHitResult.decode(readValue(buffer)!); case 198: - return GeckoPref.decode(readValue(buffer)!); + return DownloadState.decode(readValue(buffer)!); case 199: - return MlProgressData.decode(readValue(buffer)!); + return ShareInternetResourceState.decode(readValue(buffer)!); case 200: - return ContainerSiteAssignment.decode(readValue(buffer)!); + return AddonCollection.decode(readValue(buffer)!); case 201: - return GeckoHeader.decode(readValue(buffer)!); + return GeckoPref.decode(readValue(buffer)!); case 202: - return GeckoFetchRequest.decode(readValue(buffer)!); + return MlProgressData.decode(readValue(buffer)!); case 203: - return GeckoFetchResponse.decode(readValue(buffer)!); + return ContainerSiteAssignment.decode(readValue(buffer)!); case 204: - return BookmarkNode.decode(readValue(buffer)!); + return GeckoHeader.decode(readValue(buffer)!); case 205: + return GeckoFetchRequest.decode(readValue(buffer)!); + case 206: + return GeckoFetchResponse.decode(readValue(buffer)!); + case 207: + return BookmarkNode.decode(readValue(buffer)!); + case 208: return BookmarkInfo.decode(readValue(buffer)!); + case 209: + return SitePermissions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -6826,6 +6985,29 @@ class GeckoDeleteBrowsingDataController { return; } } + + /// Clear browsing data for a specific host/domain + Future clearDataForHost(String host, List dataTypes) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.clearDataForHost$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([host, dataTypes]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } class GeckoHistoryApi { @@ -7556,3 +7738,131 @@ class GeckoBookmarksApi { } } } + +/// API for managing site permissions stored in GeckoView +class GeckoSitePermissionsApi { + /// Constructor for [GeckoSitePermissionsApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoSitePermissionsApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Get permissions for origin (single source of truth from GeckoView) + Future getSitePermissions(String origin, bool private) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.getSitePermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([origin, private]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as SitePermissions?); + } + } + + /// Save/update permissions (persisted by GeckoView) + Future setSitePermissions(SitePermissions permissions, bool private) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.setSitePermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([permissions, private]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + /// Delete permissions for origin (removed from GeckoView storage) + Future deleteSitePermissions(String origin, bool private) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSitePermissionsApi.deleteSitePermissions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([origin, private]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } +} + +/// Native wrapper for Mozilla's Public Suffix List +class GeckoPublicSuffixListApi { + /// Constructor for [GeckoPublicSuffixListApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoPublicSuffixListApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Get base domain (eTLD+1) from host using Mozilla's Public Suffix List + /// Returns the host unchanged if PSL lookup fails + Future getPublicSuffixPlusOne(String host) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPublicSuffixListApi.getPublicSuffixPlusOne$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([host]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as String?)!; + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 4f1e44bf..875f3447 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1417,6 +1417,25 @@ abstract class GeckoDeleteBrowsingDataController { @async void clearDataForSessionContext(String contextId); + + /// Clear browsing data for a specific host/domain + @async + void clearDataForHost(String host, List dataTypes); +} + +/// 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, } @HostApi() @@ -1713,3 +1732,96 @@ abstract class GeckoBookmarksApi { @async bool deleteNode(String guid); } + +// ============================================================================= +// Site Permissions API +// ============================================================================= + +/// 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, +} + +/// Site permissions data structure +class SitePermissions { + final String origin; + final SitePermissionStatus? camera; + final SitePermissionStatus? microphone; + final SitePermissionStatus? location; + final SitePermissionStatus? notification; + final SitePermissionStatus? persistentStorage; + final SitePermissionStatus? crossOriginStorageAccess; + final SitePermissionStatus? mediaKeySystemAccess; + final SitePermissionStatus? localDeviceAccess; + final SitePermissionStatus? localNetworkAccess; + final AutoplayStatus? autoplayAudible; + final AutoplayStatus? autoplayInaudible; + final int savedAt; + + 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, + this.savedAt = 0, + }); +} + +/// API for managing site permissions stored in GeckoView +@HostApi() +abstract class GeckoSitePermissionsApi { + /// Get permissions for origin (single source of truth from GeckoView) + @async + SitePermissions? getSitePermissions(String origin, bool private); + + /// Save/update permissions (persisted by GeckoView) + @async + void setSitePermissions(SitePermissions permissions, bool private); + + /// Delete permissions for origin (removed from GeckoView storage) + @async + void deleteSitePermissions(String origin, bool private); +} + +// ============================================================================= +// Public Suffix List API +// ============================================================================= + +/// Native wrapper for Mozilla's Public Suffix List +@HostApi() +abstract class GeckoPublicSuffixListApi { + /// Get base domain (eTLD+1) from host using Mozilla's Public Suffix List + /// Returns the host unchanged if PSL lookup fails + @async + String getPublicSuffixPlusOne(String host); +}