From 2c00504b42ae9178b3f136aedc2b9fa631de4a45 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 16 Jan 2026 07:36:38 +0100 Subject: [PATCH] implement tracking protection per site --- app/lib/core/routing/routes.dart | 1 + app/lib/core/routing/routes.g.dart | 27 ++ app/lib/core/routing/routes.settings.dart | 12 + .../repositories/tracking_protection.dart | 77 ++++++ .../repositories/tracking_protection.g.dart | 86 +++++++ .../sheets/site_permissions_provider.dart | 12 +- .../sheets/site_permissions_provider.g.dart | 4 +- .../sheets/tracking_protection_provider.dart | 34 +++ .../tracking_protection_provider.g.dart | 95 +++++++ .../sheets/tracking_protection_section.dart | 102 ++++++++ .../presentation/widgets/sheets/view_tab.dart | 6 + .../screens/privacy_security_settings.dart | 17 ++ .../tracking_protection_exceptions.dart | 225 +++++++++++++++++ .../api/GeckoBrowserApiImpl.kt | 2 + .../api/GeckoTrackingProtectionApiImpl.kt | 78 ++++++ .../components/UseCases.kt | 2 +- .../pigeons/Gecko.g.kt | 217 ++++++++++++++++ .../lib/flutter_mozilla_components.dart | 4 +- .../lib/src/pigeons/gecko.g.dart | 233 ++++++++++++++++++ .../pigeons/gecko.dart | 56 +++++ 20 files changed, 1277 insertions(+), 13 deletions(-) create mode 100644 app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.dart create mode 100644 app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart create mode 100644 app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart create mode 100644 app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.g.dart create mode 100644 app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart create mode 100644 app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart create mode 100644 packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTrackingProtectionApiImpl.kt diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 8609f7ae..f5bce488 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -64,6 +64,7 @@ import 'package:weblibre/features/settings/presentation/screens/privacy_security import 'package:weblibre/features/settings/presentation/screens/search_content_settings.dart'; import 'package:weblibre/features/settings/presentation/screens/settings.dart'; import 'package:weblibre/features/settings/presentation/screens/tabs_behavior_settings.dart'; +import 'package:weblibre/features/settings/presentation/screens/tracking_protection_exceptions.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening.dart'; import 'package:weblibre/features/settings/presentation/screens/web_engine_hardening_group.dart'; import 'package:weblibre/features/tor/presentation/screens/tor_proxy.dart'; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 154852f6..dc4e7dcf 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -146,6 +146,11 @@ RouteBase get $settingsRoute => GoRouteData.$route( name: 'AddonCollectionRoute', factory: $AddonCollectionRoute._fromState, ), + GoRouteData.$route( + path: 'tracking_protection_exceptions', + name: 'TrackingProtectionExceptionsRoute', + factory: $TrackingProtectionExceptionsRoute._fromState, + ), ], ); @@ -446,6 +451,28 @@ mixin $AddonCollectionRoute on GoRouteData { void replace(BuildContext context) => context.replace(location); } +mixin $TrackingProtectionExceptionsRoute on GoRouteData { + static TrackingProtectionExceptionsRoute _fromState(GoRouterState state) => + TrackingProtectionExceptionsRoute(); + + @override + String get location => + GoRouteData.$location('/settings/tracking_protection_exceptions'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $browserRoute => GoRouteData.$route( path: '/browser', name: 'BrowserRoute', diff --git a/app/lib/core/routing/routes.settings.dart b/app/lib/core/routing/routes.settings.dart index 46c21c36..da7429f5 100644 --- a/app/lib/core/routing/routes.settings.dart +++ b/app/lib/core/routing/routes.settings.dart @@ -71,6 +71,10 @@ part of 'routes.dart'; name: 'AddonCollectionRoute', path: 'addon_collection', ), + TypedGoRoute( + name: 'TrackingProtectionExceptionsRoute', + path: 'tracking_protection_exceptions', + ), ], ) class SettingsRoute extends GoRouteData with $SettingsRoute { @@ -182,3 +186,11 @@ class WebEngineHardeningGroupRoute extends GoRouteData return WebEngineHardeningGroupScreen(groupName: group); } } + +class TrackingProtectionExceptionsRoute extends GoRouteData + with $TrackingProtectionExceptionsRoute { + @override + Widget build(BuildContext context, GoRouterState state) { + return const TrackingProtectionExceptionsScreen(); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.dart b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.dart new file mode 100644 index 00000000..9ef9f694 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.dart @@ -0,0 +1,77 @@ +/* + * 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 'tracking_protection.g.dart'; + +/// Repository for managing per-site Enhanced Tracking Protection exceptions +/// +/// Wraps the GeckoTrackingProtectionApi and handles state invalidation +/// automatically after mutations. +@Riverpod(keepAlive: true) +class TrackingProtectionRepository extends _$TrackingProtectionRepository { + final _api = GeckoTrackingProtectionApi(); + + /// Check if a tab has a tracking protection exception + /// + /// Returns true if ETP is disabled for this site + Future containsException(String tabId) { + return _api.containsException(tabId); + } + + /// Add tracking protection exception for a tab (disable ETP for this site) + Future addException(String tabId) async { + await _api.addException(tabId); + await _invalidateWithDelay(); + } + + /// Remove tracking protection exception for a tab (enable ETP for this site) + Future removeException(String tabId) async { + await _api.removeException(tabId); + await _invalidateWithDelay(); + } + + /// Remove a specific exception by URL and refresh the exceptions list + Future removeExceptionByUrl(String url) async { + await _api.removeExceptionByUrl(url); + await _invalidateWithDelay(); + } + + /// Remove all tracking protection exceptions + Future removeAllExceptions() async { + await _api.removeAllExceptions(); + await _invalidateWithDelay(); + } + + /// Helper method to invalidate repository state after mutations with delay + Future _invalidateWithDelay() async { + await Future.delayed(const Duration(milliseconds: 100)).whenComplete(() { + if (ref.mounted) { + ref.invalidateSelf(); + } + }); + } + + @override + Future> build() { + return _api.fetchExceptions(); + } +} diff --git a/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart new file mode 100644 index 00000000..da740b18 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/repositories/tracking_protection.g.dart @@ -0,0 +1,86 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tracking_protection.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Repository for managing per-site Enhanced Tracking Protection exceptions +/// +/// Wraps the GeckoTrackingProtectionApi and handles state invalidation +/// automatically after mutations. + +@ProviderFor(TrackingProtectionRepository) +final trackingProtectionRepositoryProvider = + TrackingProtectionRepositoryProvider._(); + +/// Repository for managing per-site Enhanced Tracking Protection exceptions +/// +/// Wraps the GeckoTrackingProtectionApi and handles state invalidation +/// automatically after mutations. +final class TrackingProtectionRepositoryProvider + extends + $AsyncNotifierProvider< + TrackingProtectionRepository, + List + > { + /// Repository for managing per-site Enhanced Tracking Protection exceptions + /// + /// Wraps the GeckoTrackingProtectionApi and handles state invalidation + /// automatically after mutations. + TrackingProtectionRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'trackingProtectionRepositoryProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$trackingProtectionRepositoryHash(); + + @$internal + @override + TrackingProtectionRepository create() => TrackingProtectionRepository(); +} + +String _$trackingProtectionRepositoryHash() => + r'6d4407034b2c67317d0a0e106002f1785fd86a34'; + +/// Repository for managing per-site Enhanced Tracking Protection exceptions +/// +/// Wraps the GeckoTrackingProtectionApi and handles state invalidation +/// automatically after mutations. + +abstract class _$TrackingProtectionRepository + extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + AsyncValue>, + List + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue>, + List + >, + AsyncValue>, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} 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 index 01ac5229..851380fe 100644 --- 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 @@ -28,17 +28,14 @@ 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 { +Future publicSuffixPlusOne(Ref ref, String host) { final api = GeckoPublicSuffixListApi(); return api.getPublicSuffixPlusOne(host); } @@ -49,10 +46,7 @@ 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, - }; + return {ClearDataType.allSiteData, ClearDataType.authSessions}; } void toggle(ClearDataType type) { 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 index 071ee6da..858d481b 100644 --- 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 @@ -68,7 +68,7 @@ final class SitePermissionsProvider } } -String _$sitePermissionsHash() => r'c605e9a1acb261ec46659431b5bb1e105b8aa2a1'; +String _$sitePermissionsHash() => r'785af0f938bfe53799fc8b6ee8be71015afbc86c'; /// Provider to fetch site permissions for a given origin @@ -147,7 +147,7 @@ final class PublicSuffixPlusOneProvider } String _$publicSuffixPlusOneHash() => - r'e259f66e3ebf278468223d8e25618eb9f67a3504'; + r'2871740bee033e634047b1374ae8dfad508f2cb2'; /// Provider to get the public suffix plus one (eTLD+1) for a host diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart new file mode 100644 index 00000000..6b48f909 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart @@ -0,0 +1,34 @@ +/* + * 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:riverpod_annotation/riverpod_annotation.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart'; + +part 'tracking_protection_provider.g.dart'; + +/// Provider that checks if a specific tab has a tracking protection exception +/// Automatically rebuilds when repository changes after mutations +@Riverpod() +Future hasTrackingProtectionException(Ref ref, String tabId) { + ref.watch(trackingProtectionRepositoryProvider); + + return ref + .read(trackingProtectionRepositoryProvider.notifier) + .containsException(tabId); +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.g.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.g.dart new file mode 100644 index 00000000..a3f4e0c7 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.g.dart @@ -0,0 +1,95 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tracking_protection_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Provider that checks if a specific tab has a tracking protection exception +/// Automatically rebuilds when repository changes after mutations + +@ProviderFor(hasTrackingProtectionException) +final hasTrackingProtectionExceptionProvider = + HasTrackingProtectionExceptionFamily._(); + +/// Provider that checks if a specific tab has a tracking protection exception +/// Automatically rebuilds when repository changes after mutations + +final class HasTrackingProtectionExceptionProvider + extends $FunctionalProvider, bool, FutureOr> + with $FutureModifier, $FutureProvider { + /// Provider that checks if a specific tab has a tracking protection exception + /// Automatically rebuilds when repository changes after mutations + HasTrackingProtectionExceptionProvider._({ + required HasTrackingProtectionExceptionFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'hasTrackingProtectionExceptionProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$hasTrackingProtectionExceptionHash(); + + @override + String toString() { + return r'hasTrackingProtectionExceptionProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return hasTrackingProtectionException(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is HasTrackingProtectionExceptionProvider && + other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$hasTrackingProtectionExceptionHash() => + r'3c6c2d4a2c75d52c33211bc8da8191185fb6c542'; + +/// Provider that checks if a specific tab has a tracking protection exception +/// Automatically rebuilds when repository changes after mutations + +final class HasTrackingProtectionExceptionFamily extends $Family + with $FunctionalFamilyOverride, String> { + HasTrackingProtectionExceptionFamily._() + : super( + retry: null, + name: r'hasTrackingProtectionExceptionProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Provider that checks if a specific tab has a tracking protection exception + /// Automatically rebuilds when repository changes after mutations + + HasTrackingProtectionExceptionProvider call(String tabId) => + HasTrackingProtectionExceptionProvider._(argument: tabId, from: this); + + @override + String toString() => r'hasTrackingProtectionExceptionProvider'; +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart new file mode 100644 index 00000000..c16c3b48 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart @@ -0,0 +1,102 @@ +/* + * 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:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_provider.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +/// Section widget displaying Enhanced Tracking Protection toggle +class TrackingProtectionSection extends HookConsumerWidget { + final String tabId; + + const TrackingProtectionSection({required this.tabId, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasExceptionAsync = ref.watch( + hasTrackingProtectionExceptionProvider(tabId), + ); + + return hasExceptionAsync.when( + data: (hasException) => _TrackingProtectionTile( + tabId: tabId, + isEnabled: !hasException, // ETP enabled when NOT in exceptions + ), + loading: () => const SizedBox.shrink(), + error: (error, stack) => const SizedBox.shrink(), + ); + } +} + +class _TrackingProtectionTile extends ConsumerWidget { + final String tabId; + final bool isEnabled; + + const _TrackingProtectionTile({required this.tabId, required this.isEnabled}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SwitchListTile.adaptive( + value: isEnabled, + onChanged: (enabled) => _toggleProtection(context, ref, enabled), + title: const Text('Enhanced Tracking Protection'), + subtitle: Text( + isEnabled + ? 'Trackers on this site are being blocked' + : 'Trackers on this site are allowed', + ), + secondary: Icon( + isEnabled ? Icons.shield : Icons.shield_outlined, + color: isEnabled + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } + + Future _toggleProtection( + BuildContext context, + WidgetRef ref, + bool enabled, + ) async { + try { + if (enabled) { + // Remove exception to enable ETP + await ref + .read(trackingProtectionRepositoryProvider.notifier) + .removeException(tabId); + } else { + // Add exception to disable ETP + await ref + .read(trackingProtectionRepositoryProvider.notifier) + .addException(tabId); + } + + // Reload tab to apply changes + await ref.read(selectedTabSessionProvider).reload(); + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Failed to toggle tracking protection: $e'); + } + } + } +} 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 c964934f..b86347bb 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 @@ -29,6 +29,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget 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/features/geckoview/features/browser/presentation/widgets/sheets/tracking_protection_section.dart'; import 'package:weblibre/presentation/widgets/website_title_tile.dart'; class ClampingScrollPhysicsWithoutImplicit extends ClampingScrollPhysics { @@ -208,6 +209,11 @@ class ViewTabSheetWidget extends HookConsumerWidget { ), ), const Divider(), + // Tracking Protection Section + TrackingProtectionSection( + tabId: initialTabState.id, + ), + const Divider(), // Permissions Section PermissionsSection( origin: initialTabState.url.origin, diff --git a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart index f8e43f75..aa60a13a 100644 --- a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart +++ b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart @@ -21,6 +21,7 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/routes.dart'; @@ -87,11 +88,27 @@ class _TrackingProtectionSection extends StatelessWidget { _EnhancedTrackingProtectionSection(), _BounceTrackingProtectionTile(), _QueryParameterStrippingSection(), + _TrackingProtectionExceptionsTile(), ], ); } } +class _TrackingProtectionExceptionsTile extends StatelessWidget { + const _TrackingProtectionExceptionsTile(); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(MdiIcons.shieldOffOutline), + title: const Text('Tracking Protection Exceptions'), + subtitle: const Text('Sites where tracking protection is disabled'), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.push('/settings/tracking_protection_exceptions'), + ); + } +} + class _ConnectionSecuritySection extends StatelessWidget { const _ConnectionSecuritySection(); diff --git a/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart b/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart new file mode 100644 index 00000000..a585fe1d --- /dev/null +++ b/app/lib/features/settings/presentation/screens/tracking_protection_exceptions.dart @@ -0,0 +1,225 @@ +/* + * 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_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/repositories/tracking_protection.dart'; +import 'package:weblibre/presentation/widgets/url_icon.dart'; +import 'package:weblibre/utils/ui_helper.dart'; + +/// Screen to view and manage tracking protection exceptions +/// +/// Shows list of all sites where ETP is disabled, with options +/// to remove individual exceptions or remove all exceptions. +class TrackingProtectionExceptionsScreen extends HookConsumerWidget { + const TrackingProtectionExceptionsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final exceptionsAsync = ref.watch(trackingProtectionRepositoryProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Tracking Protection Exceptions'), + actions: [ + exceptionsAsync.maybeWhen( + data: (exceptions) => exceptions.isNotEmpty + ? MenuAnchor( + builder: (context, controller, child) => IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + ), + menuChildren: [ + MenuItemButton( + leadingIcon: const Icon(Icons.delete_sweep), + onPressed: () => _showDeleteAllDialog(context, ref), + child: const Text('Delete All'), + ), + ], + ) + : const SizedBox.shrink(), + orElse: () => const SizedBox.shrink(), + ), + ], + ), + body: exceptionsAsync.when( + data: (exceptions) { + if (exceptions.isEmpty) { + return const _EmptyState(); + } + + return ListView.builder( + itemCount: exceptions.length, + itemBuilder: (context, index) { + final exception = exceptions[index]; + return _ExceptionTile( + exception: exception, + onDelete: () => _deleteException(context, ref, exception), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => _ErrorState(error: error.toString()), + ), + ); + } + + Future _showDeleteAllDialog(BuildContext context, WidgetRef ref) async { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete All Exceptions?'), + content: const Text( + 'This will re-enable tracking protection for all exception sites.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + try { + await ref + .read(trackingProtectionRepositoryProvider.notifier) + .removeAllExceptions(); + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Failed to delete exceptions: $e'); + } + } + }, + child: const Text('Delete'), + ), + ], + ), + ); + } + + Future _deleteException( + BuildContext context, + WidgetRef ref, + TrackingProtectionException exception, + ) async { + try { + await ref + .read(trackingProtectionRepositoryProvider.notifier) + .removeExceptionByUrl(exception.url); + } catch (e) { + if (context.mounted) { + showErrorMessage(context, 'Failed to remove exception: $e'); + } + } + } +} + +class _ExceptionTile extends StatelessWidget { + final TrackingProtectionException exception; + final VoidCallback onDelete; + + const _ExceptionTile({required this.exception, required this.onDelete}); + + @override + Widget build(BuildContext context) { + final uri = Uri.tryParse(exception.url); + + return ListTile( + leading: uri != null + ? UrlIcon([uri], iconSize: 24) + : const Icon(MdiIcons.shieldOutline), + title: Text(exception.url), + trailing: IconButton( + icon: const Icon(Icons.close), + onPressed: onDelete, + tooltip: 'Remove exception', + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + const _EmptyState(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.shield_outlined, + size: 64, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text('No exceptions', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Text( + 'Sites added to exceptions will appear here', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + } +} + +class _ErrorState extends StatelessWidget { + final String error; + + const _ErrorState({required this.error}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 64), + const SizedBox(height: 16), + Text( + 'Error loading exceptions', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + error, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} 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 52465b7c..391e87f3 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 @@ -35,6 +35,7 @@ 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.GeckoTrackingProtectionApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoLogging import eu.weblibre.flutter_mozilla_components.pigeons.GeckoMlApi import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPrefApi @@ -268,6 +269,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi { GeckoBookmarksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBookmarksApiImpl()) GeckoSitePermissionsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSitePermissionsApiImpl()) GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext)) + GeckoTrackingProtectionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTrackingProtectionApiImpl()) // 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/GeckoTrackingProtectionApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTrackingProtectionApiImpl.kt new file mode 100644 index 00000000..e10769f2 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoTrackingProtectionApiImpl.kt @@ -0,0 +1,78 @@ +/* + * 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.GeckoTrackingProtectionApi +import eu.weblibre.flutter_mozilla_components.pigeons.TrackingProtectionException +import mozilla.components.concept.engine.content.blocking.TrackingProtectionException as MozillaTrackingProtectionException + +/** + * Implementation of GeckoTrackingProtectionApi that manages per-site + * Enhanced Tracking Protection exceptions using Mozilla Android Components' + * TrackingProtectionUseCases. + */ +class GeckoTrackingProtectionApiImpl : GeckoTrackingProtectionApi { + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override fun containsException(tabId: String, callback: (Result) -> Unit) { + try { + components.useCases.trackingProtectionUseCases.containsException(tabId) { hasException -> + callback(Result.success(hasException)) + } + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun addException(tabId: String) { + components.useCases.trackingProtectionUseCases.addException(tabId) + } + + override fun removeException(tabId: String) { + components.useCases.trackingProtectionUseCases.removeException(tabId) + } + + override fun removeExceptionByUrl(url: String, callback: (Result) -> Unit) { + try { + // Create a simple TrackingProtectionException implementation for removal + val exception = object : MozillaTrackingProtectionException { + override val url: String = url + } + components.useCases.trackingProtectionUseCases.removeException(exception) + // The Mozilla API is synchronous, callback immediately after completion + callback(Result.success(Unit)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun fetchExceptions(callback: (Result>) -> Unit) { + try { + components.useCases.trackingProtectionUseCases.fetchExceptions { mozillaExceptions -> + val pigeonExceptions = mozillaExceptions.map { mozillaException -> + TrackingProtectionException(url = mozillaException.url) + } + callback(Result.success(pigeonExceptions)) + } + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + override fun removeAllExceptions(callback: (Result) -> Unit) { + try { + components.useCases.trackingProtectionUseCases.removeAllExceptions { + callback(Result.success(Unit)) + } + } catch (e: Exception) { + callback(Result.failure(e)) + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/UseCases.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/UseCases.kt index 6517ce6b..0a8a541e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/UseCases.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/components/UseCases.kt @@ -51,5 +51,5 @@ class UseCases( val appLinksUseCases by lazy { AppLinksUseCases(context) } - //val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } + val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } } 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 9b520667..fb274509 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 @@ -2825,6 +2825,41 @@ data class SitePermissions ( override fun hashCode(): Int = toList().hashCode() } + +/** + * Tracking protection exception for a site + * + * This represents a site that has been added to the exceptions list, + * meaning tracking protection is disabled for this specific site. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class TrackingProtectionException ( + val url: String +) + { + companion object { + fun fromList(pigeonVar_list: List): TrackingProtectionException { + val url = pigeonVar_list[0] as String + return TrackingProtectionException(url) + } + } + fun toList(): List { + return listOf( + url, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is TrackingProtectionException) { + 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) { @@ -3233,6 +3268,11 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { SitePermissions.fromList(it) } } + 210.toByte() -> { + return (readValue(buffer) as? List)?.let { + TrackingProtectionException.fromList(it) + } + } else -> super.readValueOfType(type, buffer) } } @@ -3562,6 +3602,10 @@ private open class GeckoPigeonCodec : StandardMessageCodec() { stream.write(209) writeValue(stream, value.toList()) } + is TrackingProtectionException -> { + stream.write(210) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } @@ -6717,3 +6761,176 @@ interface GeckoPublicSuffixListApi { } } } +/** + * API for managing per-site tracking protection exceptions + * + * This API wraps Mozilla Android Components' TrackingProtectionUseCases + * to allow Flutter code to add/remove/check tracking protection exceptions + * on a per-site basis. + * + * Generated interface from Pigeon that represents a handler of messages from Flutter. + */ +interface GeckoTrackingProtectionApi { + /** + * Check if a tab has a tracking protection exception + * + * Uses callback pattern to match Mozilla Android Components API. + * Returns true if the site is in the exceptions list (ETP disabled). + */ + fun containsException(tabId: String, callback: (Result) -> Unit) + /** + * Add tracking protection exception for a tab (disable ETP for this site) + * + * This adds the current tab's URL to the exceptions list. + * ETP will be disabled for this site until the exception is removed. + */ + fun addException(tabId: String) + /** + * Remove tracking protection exception for a tab (enable ETP for this site) + * + * This removes the current tab's URL from the exceptions list. + * ETP will be re-enabled for this site. + */ + fun removeException(tabId: String) + /** + * Remove a specific exception by URL + * + * Alternative to removeException(tabId) for cases where you + * have a URL rather than a tabId. + */ + fun removeExceptionByUrl(url: String, callback: (Result) -> Unit) + /** + * Fetch all tracking protection exceptions + * + * Returns list of all sites that have exceptions (ETP disabled). + */ + fun fetchExceptions(callback: (Result>) -> Unit) + /** + * Remove all tracking protection exceptions + * + * This re-enables ETP for all exception sites. + */ + fun removeAllExceptions(callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoTrackingProtectionApi. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoTrackingProtectionApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoTrackingProtectionApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.containsException$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String + api.containsException(tabIdArg) { 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.GeckoTrackingProtectionApi.addException$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String + val wrapped: List = try { + api.addException(tabIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeException$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val tabIdArg = args[0] as String + val wrapped: List = try { + api.removeException(tabIdArg) + listOf(null) + } catch (exception: Throwable) { + GeckoPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeExceptionByUrl$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val urlArg = args[0] as String + api.removeExceptionByUrl(urlArg) { 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.GeckoTrackingProtectionApi.fetchExceptions$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.fetchExceptions{ 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.GeckoTrackingProtectionApi.removeAllExceptions$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.removeAllExceptions{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + reply.reply(GeckoPigeonUtils.wrapResult(null)) + } + } + } + } 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 cec71388..6319804c 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -58,6 +58,7 @@ export 'src/pigeons/gecko.g.dart' GeckoSitePermissionsApi, GeckoSuggestion, GeckoSuggestionType, + GeckoTrackingProtectionApi, GeoHitResult, HistoryMetadataKey, HitResult, @@ -75,10 +76,11 @@ export 'src/pigeons/gecko.g.dart' Resource, ResourceSize, SecurityInfoState, - SitePermissions, SitePermissionStatus, + SitePermissions, TabContent, TabContentState, + TrackingProtectionException, TrackingProtectionPolicy, UnknownHitResult, VideoHitResult, 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 d077e6f8..813e1502 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -3613,6 +3613,51 @@ class SitePermissions { ; } +/// Tracking protection exception for a site +/// +/// This represents a site that has been added to the exceptions list, +/// meaning tracking protection is disabled for this specific site. +class TrackingProtectionException { + TrackingProtectionException({ + required this.url, + }); + + String url; + + List _toList() { + return [ + url, + ]; + } + + Object encode() { + return _toList(); } + + static TrackingProtectionException decode(Object result) { + result as List; + return TrackingProtectionException( + url: result[0]! as String, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! TrackingProtectionException || 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(); @@ -3864,6 +3909,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is SitePermissions) { buffer.putUint8(209); writeValue(buffer, value.encode()); + } else if (value is TrackingProtectionException) { + buffer.putUint8(210); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -4062,6 +4110,8 @@ class _PigeonCodec extends StandardMessageCodec { return BookmarkInfo.decode(readValue(buffer)!); case 209: return SitePermissions.decode(readValue(buffer)!); + case 210: + return TrackingProtectionException.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -7866,3 +7916,186 @@ class GeckoPublicSuffixListApi { } } } + +/// API for managing per-site tracking protection exceptions +/// +/// This API wraps Mozilla Android Components' TrackingProtectionUseCases +/// to allow Flutter code to add/remove/check tracking protection exceptions +/// on a per-site basis. +class GeckoTrackingProtectionApi { + /// Constructor for [GeckoTrackingProtectionApi]. 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. + GeckoTrackingProtectionApi({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; + + /// Check if a tab has a tracking protection exception + /// + /// Uses callback pattern to match Mozilla Android Components API. + /// Returns true if the site is in the exceptions list (ETP disabled). + Future containsException(String tabId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.containsException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + 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 bool?)!; + } + } + + /// Add tracking protection exception for a tab (disable ETP for this site) + /// + /// This adds the current tab's URL to the exceptions list. + /// ETP will be disabled for this site until the exception is removed. + Future addException(String tabId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.addException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + 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; + } + } + + /// Remove tracking protection exception for a tab (enable ETP for this site) + /// + /// This removes the current tab's URL from the exceptions list. + /// ETP will be re-enabled for this site. + Future removeException(String tabId) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeException$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([tabId]); + 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; + } + } + + /// Remove a specific exception by URL + /// + /// Alternative to removeException(tabId) for cases where you + /// have a URL rather than a tabId. + Future removeExceptionByUrl(String url) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeExceptionByUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([url]); + 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; + } + } + + /// Fetch all tracking protection exceptions + /// + /// Returns list of all sites that have exceptions (ETP disabled). + Future> fetchExceptions() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.fetchExceptions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + 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 List?)!.cast(); + } + } + + /// Remove all tracking protection exceptions + /// + /// This re-enables ETP for all exception sites. + Future removeAllExceptions() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTrackingProtectionApi.removeAllExceptions$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + 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; + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 875f3447..ed6147f6 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1825,3 +1825,59 @@ abstract class GeckoPublicSuffixListApi { @async String getPublicSuffixPlusOne(String host); } + +/// Tracking protection exception for a site +/// +/// This represents a site that has been added to the exceptions list, +/// meaning tracking protection is disabled for this specific site. +class TrackingProtectionException { + final String url; + + TrackingProtectionException({required this.url}); +} + +/// API for managing per-site tracking protection exceptions +/// +/// This API wraps Mozilla Android Components' TrackingProtectionUseCases +/// to allow Flutter code to add/remove/check tracking protection exceptions +/// on a per-site basis. +@HostApi() +abstract class GeckoTrackingProtectionApi { + /// Check if a tab has a tracking protection exception + /// + /// Uses callback pattern to match Mozilla Android Components API. + /// Returns true if the site is in the exceptions list (ETP disabled). + @async + bool containsException(String tabId); + + /// Add tracking protection exception for a tab (disable ETP for this site) + /// + /// This adds the current tab's URL to the exceptions list. + /// ETP will be disabled for this site until the exception is removed. + void addException(String tabId); + + /// Remove tracking protection exception for a tab (enable ETP for this site) + /// + /// This removes the current tab's URL from the exceptions list. + /// ETP will be re-enabled for this site. + void removeException(String tabId); + + /// Remove a specific exception by URL + /// + /// Alternative to removeException(tabId) for cases where you + /// have a URL rather than a tabId. + @async + void removeExceptionByUrl(String url); + + /// Fetch all tracking protection exceptions + /// + /// Returns list of all sites that have exceptions (ETP disabled). + @async + List fetchExceptions(); + + /// Remove all tracking protection exceptions + /// + /// This re-enables ETP for all exception sites. + @async + void removeAllExceptions(); +}