# External Download Manager Feature - Implementation Plan ## Overview Implement support for external download managers (ADM, 1DM, AB DM) as requested in GitHub issue #146. The feature uses a database-stored setting (similar to AppLinks) rather than SharedPreferences, and configures Mozilla's DownloadsFeature to forward downloads to third-party apps. ## Reference Implementation (Fenix) - **Preference Key**: `pref_key_external_download_manager` - **Usage in Fenix**: - `fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt:759` - DownloadsFeature configuration - `fenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt:93` - DownloadsFeature in addons - `fenix/app/src/main/res/xml/downloads_settings_preferences.xml:8` - Settings UI XML - `fenix/app/src/main/res/values/preference_keys.xml:503` - Preference key definition ## WebLibre Pattern Reference (AppLinks) The implementation should follow the existing AppLinks pattern as a reference: - **Model**: `app/lib/features/user/data/models/general_settings.dart` - AppLinksMode not here (it's in native), but this shows the pattern - **Repository**: `app/lib/features/user/domain/repositories/general_settings.dart:37-195` - GeneralSettingsRepository pattern - **Native Storage**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt:325-352` - AppLinks storage in SharedPreferences (our implementation will use global state set by Flutter) - **Native Usage**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt:60-74` - shouldOpenLinksInApp/shouldPromptOpenLinksInApp pattern - **Pigeon API**: `packages/flutter_mozilla_components/pigeons/gecko.dart:643-653` - AppLinksMode enum - **Provider**: `app/lib/features/geckoview/features/browser/domain/providers.dart:382-395` - AppLinksModeNotifier - **Service**: `packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart:142-150` - setAppLinksMode/getAppLinksMode ## Architecture ### Data Flow 1. **Database Storage**: Setting stored in `user.db` -> `setting` table with partition_key='general' 2. **Flutter Layer**: GeneralSettings model manages the setting state 3. **Pigeon Bridge**: `setUseExternalDownloadManager(bool)`/`getUseExternalDownloadManager()` sync to native 4. **Native Layer**: GlobalComponents stores value as `var useExternalDownloadManager: Boolean` 5. **DownloadsFeature**: Uses `shouldForwardToThirdParties = { GlobalComponents.useExternalDownloadManager }` ### Key Components - **Setting Type**: Boolean (default: false) - **Storage Location**: user.db, setting table, partition_key='general', key='useExternalDownloadManager' - **Native Integration**: Mozilla Android Components DownloadsFeature's shouldForwardToThirdParties callback ## Implementation Steps ### Phase 1: Data Model & Database Layer #### Step 1.1 - Update GeneralSettings Model **File**: `app/lib/features/user/data/models/general_settings.dart` **Changes Required**: - Add new field declaration: `final bool useExternalDownloadManager;` - Add field to constructor parameters - Add field to `withDefaults()` constructor (default value: `false`) - Add field to `fromJson()`/`toJson()` methods via @JsonSerializable annotation - Add field to `hashParameters` getter for FastEquatable **Reference**: Look at `pullToRefreshEnabled` field (line 83) as a template #### Step 1.2 - Update GeneralSettingsRepository **File**: `app/lib/features/user/domain/repositories/general_settings.dart` **Changes Required**: - Add deserialization logic in `_deserializeSettings()` method (around line 142) - Pattern: ```dart 'useExternalDownloadManager': settings['useExternalDownloadManager']?.readAs( DriftSqlType.bool, db.typeMapping, ), ``` - This follows the same pattern as existing fields like `pullToRefreshEnabled` (line 125-128) **Reference**: `_deserializeSettings()` method (lines 39-143) shows how all settings are deserialized ### Phase 2: Pigeon API Definition #### Step 2.1 - Add Pigeon API Methods **File**: `packages/flutter_mozilla_components/pigeons/gecko.dart` **Location**: Find `GeckoEngineSettingsApi` interface (around line 1004-1015) **Changes Required**: - Add method declaration before closing brace: ```dart void setUseExternalDownloadManager(bool enabled); bool getUseExternalDownloadManager(); ``` **Reference**: Similar to `setAppLinksMode` and `getAppLinksMode` methods (lines 1012-1014) **Important**: After modifying this file, must run `melos build-pigeons --no-select` to regenerate interfaces ### Phase 3: Flutter Service Layer #### Step 3.1 - Update GeckoEngineSettingsService **File**: `packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart` **Location**: After `getAppLinksMode()` method (around line 148-151) **Changes Required**: - Add two methods to wrap Pigeon API calls: ```dart Future setUseExternalDownloadManager(bool enabled) { return _api.setUseExternalDownloadManager(enabled); } Future getUseExternalDownloadManager() { return _api.getUseExternalDownloadManager(); } ``` **Reference**: Follow pattern of `setAppLinksMode()`/`getAppLinksMode()` methods (lines 144-150) #### Step 3.2 - Add Riverpod Provider **File**: `app/lib/features/geckoview/features/browser/domain/providers.dart` **Location**: After `AppLinksModeNotifier` class (around line 395) **Changes Required**: - Add new provider class: ```dart @Riverpod(keepAlive: true) class ExternalDownloadManagerNotifier extends _$ExternalDownloadManagerNotifier { final _service = GeckoEngineSettingsService(); Future setEnabled(bool enabled) async { await _service.setUseExternalDownloadManager(enabled); ref.invalidateSelf(); } @override Future build() { return _service.getUseExternalDownloadManager(); } } ``` **Reference**: Follow `AppLinksModeNotifier` pattern (lines 382-395) **Important**: After modifying this file, must run `melos build --no-select` to generate provider code ### Phase 4: Native Implementation (Kotlin) #### Step 4.1 - Implement Pigeon API Methods **File**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt` **Location**: After `getAppLinksMode()` method (around line 353) **Changes Required**: - Add method implementations: ```kotlin override fun setUseExternalDownloadManager(enabled: Boolean) { GlobalComponents.useExternalDownloadManager = enabled } override fun getUseExternalDownloadManager(): Boolean { return GlobalComponents.useExternalDownloadManager ?: false } ``` **Reference**: Follow `setAppLinksMode()`/`getAppLinksMode()` implementation pattern (lines 325-352) **Note**: Unlike AppLinks which uses SharedPreferences, we use a global variable that Flutter controls #### Step 4.2 - Add Global State Variable **File**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt` **Location**: After other engine settings state (around line 74) **Changes Required**: - Add companion object variable: ```kotlin var useExternalDownloadManager: Boolean = false ``` **Reference**: Similar to `engineSettingsApi` and `pullToRefreshEnabled` variables (lines 58, 61) #### Step 4.3 - Configure DownloadsFeature **File**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt` **Location**: Find `downloadsFeature.set(...)` call (around line 271-293) **Changes Required**: - Add `shouldForwardToThirdParties` parameter to `DownloadsFeature` constructor: ```kotlin downloadsFeature.set( feature = DownloadsFeature( // ... existing parameters ... onNeedToRequestPermissions = { permissions -> requestDownloadPermissionsLauncher.launch(permissions) }, shouldForwardToThirdParties = { GlobalComponents.useExternalDownloadManager }, ), owner = this, view = view, ) ``` **Reference**: Fenix implementation at `fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt:757-762` ### Phase 5: UI Implementation #### Step 5.1 - Create Downloads Settings Section **File**: `app/lib/features/settings/presentation/screens/privacy_security_settings.dart` **Location**: In the `build()` method's ListView children list (around line 50) **Changes Required**: - Add `_DownloadsSection()` to children array after `_DataManagementSection()` - Create new widget class: ```dart class _DownloadsSection extends StatelessWidget { const _DownloadsSection(); @override Widget build(BuildContext context) { return const Column( children: [ SettingSection(name: 'Downloads'), _ExternalDownloadManagerTile(), ], ); } } ``` **Reference**: Look at `_PrivacyModesSection` (lines 65-78) or `_TrackingProtectionSection` patterns #### Step 5.2 - Create Toggle Tile Widget **File**: `app/lib/features/settings/presentation/screens/privacy_security_settings.dart` **Location**: Add after the new `_DownloadsSection` class **Changes Required**: - Create `HookConsumerWidget` for the toggle: ```dart class _ExternalDownloadManagerTile extends HookConsumerWidget { const _ExternalDownloadManagerTile(); @override Widget build(BuildContext context) { final useExternalDownloadManager = ref.watch( generalSettingsWithDefaultsProvider.select((s) => s.useExternalDownloadManager), ); return SettingToggleTile( title: Text('Use external download manager'), subtitle: Text('Forward downloads to apps like ADM, 1DM, AB DM'), value: useExternalDownloadManager, onChanged: (value) async { await ref .read(generalSettingsRepositoryProvider.notifier) .updateSettings((s) => s.copyWith(useExternalDownloadManager: value)); }, ); } } ``` **Reference**: Look at `_IncognitoModeSection` or other toggle implementations in the same file **Pattern Reference**: `pullToRefreshEnabled` toggle at `app/lib/features/settings/presentation/screens/appearance_display_settings.dart:385-408` ### Phase 6: Synchronization & Initialization #### Step 6.1 - Initial Synchronization **Consideration**: When the app starts, we need to ensure the native side has the correct value. **Options**: 1. **Option A**: Add to initialization code in `app/lib/domain/services/app_initialization.dart` 2. **Option B**: Let the provider's `build()` method handle it naturally (recommended) 3. **Option C**: Add to `app/lib/features/geckoview/features/browser/domain/providers.dart` initialization **Recommended Approach**: Let the Riverpod provider handle it. When UI first renders, it will: - Read from database via `generalSettingsRepositoryProvider` - Provider's `build()` calls `getUseExternalDownloadManager()` from native - The provider pattern will handle setting the value on native side when changed **Note**: Unlike AppLinks (which reads from SharedPreferences in native code), this pattern relies on Flutter being the source of truth and syncing to native. ## Build Commands After completing implementation changes, run these commands in order: ```bash # 1. Regenerate Pigeon interfaces (after modifying gecko.dart) melos build-pigeons --no-select # 2. Regenerate Riverpod providers and models (after modifying providers.dart or models) melos build --no-select ``` ## Testing Checklist - [ ] Setting toggle appears in Privacy & Security settings - [ ] Toggle state persists across app restarts - [ ] When enabled, downloads are forwarded to external apps - [ ] When disabled, downloads use built-in manager - [ ] Setting value is correctly stored in user.db - [ ] Native side correctly reads value from Flutter - [ ] DownloadsFeature uses correct shouldForwardToThirdParties value - [ ] No SharedPreferences usage (requirement verified) ## File Modification Summary | # | File | Phase | Change Type | |---|------|-------|-------------| | 1 | `app/lib/features/user/data/models/general_settings.dart` | 1.1 | Add model field | | 2 | `app/lib/features/user/domain/repositories/general_settings.dart` | 1.2 | Add deserialization | | 3 | `packages/flutter_mozilla_components/pigeons/gecko.dart` | 2.1 | Add Pigeon API methods | | 4 | `packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart` | 3.1 | Add service wrapper methods | | 5 | `app/lib/features/geckoview/features/browser/domain/providers.dart` | 3.2 | Add Riverpod provider | | 6 | `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt` | 4.1 | Implement native methods | | 7 | `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt` | 4.2 | Add global state variable | | 8 | `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/BaseBrowserFragment.kt` | 4.3 | Configure DownloadsFeature | | 9 | `app/lib/features/settings/presentation/screens/privacy_security_settings.dart` | 5.1, 5.2 | Add UI widgets | ## Important Notes ### Design Decisions 1. **No SharedPreferences in Native Code**: Unlike AppLinks which uses SharedPreferences, this feature uses a global variable set by Flutter. This ensures Flutter is the single source of truth. 2. **Database as Storage**: Setting is stored in the database (user.db, setting table) following the same pattern as other GeneralSettings. 3. **Provider Architecture**: Uses `@Riverpod(keepAlive: true)` to ensure the provider persists across widget rebuilds. 4. **Async Provider**: The provider returns `Future` because native Pigeon calls are asynchronous. 5. **Auto-Dispose Prevention**: `keepAlive: true` ensures the provider isn't disposed when no widgets are listening, which is important for maintaining sync state. ### Mozilla Android Components Integration The `shouldForwardToThirdParties` callback in `DownloadsFeature` is invoked for each download request: - When `true`: Download is forwarded to an external app via intent - When `false`: Download is handled internally by FetchDownloadManager This is the exact same mechanism used in Fenix for the same feature. ### Code Generation Both `melos build-pigeons --no-select` and `melos build --no-select` are required because: - Pigeon generates platform channel interfaces - Riverpod generates provider code - JSON serialization generates model methods - CopyWith extension generates copyWith methods Run these commands after modifying any annotated files. ## Related Code References ### Database Schema **File**: `app/lib/features/user/data/database/definitions.drift:1-5` - Setting table structure - Uses Drift's `ANY` type for flexible value storage ### DAO Pattern **File**: `app/lib/features/user/data/database/daos/setting.dart:29-62` - `updateSetting()` method - `getAllSettingsOfPartitionKey()` method - Pattern for partition-based settings ### Settings UI Pattern **File**: `app/lib/features/settings/presentation/screens/appearance_display_settings.dart:385-408` - Example of toggle tile implementation - Shows how to watch and update settings ### Native Feature Integration **File**: `packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt:60-74` - Pattern for shouldOpenLinksInApp and shouldPromptOpenLinksInApp - Similar to how shouldForwardToThirdParties will be used ## Success Criteria Feature is complete when: 1. User can toggle "Use external download manager" in Privacy & Security settings 2. Toggle state persists in database (user.db, setting table) 3. When enabled, clicking download links prompts to open in external apps 4. When disabled, downloads use built-in download manager 5. Setting syncs correctly between Flutter and native layers 6. No SharedPreferences are used for this feature (verified requirement) 7. Code follows existing WebLibre patterns (GeneralSettings, AppLinks, etc.)