16 KiB
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 configurationfenix/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt:93- DownloadsFeature in addonsfenix/app/src/main/res/xml/downloads_settings_preferences.xml:8- Settings UI XMLfenix/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
- Database Storage: Setting stored in
user.db->settingtable with partition_key='general' - Flutter Layer: GeneralSettings model manages the setting state
- Pigeon Bridge:
setUseExternalDownloadManager(bool)/getUseExternalDownloadManager()sync to native - Native Layer: GlobalComponents stores value as
var useExternalDownloadManager: Boolean - 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
hashParametersgetter 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:
'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:
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:
Future<void> setUseExternalDownloadManager(bool enabled) { return _api.setUseExternalDownloadManager(enabled); } Future<bool> 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:
@Riverpod(keepAlive: true) class ExternalDownloadManagerNotifier extends _$ExternalDownloadManagerNotifier { final _service = GeckoEngineSettingsService(); Future<void> setEnabled(bool enabled) async { await _service.setUseExternalDownloadManager(enabled); ref.invalidateSelf(); } @override Future<bool> 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:
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:
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
shouldForwardToThirdPartiesparameter toDownloadsFeatureconstructor: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:
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
HookConsumerWidgetfor the toggle: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:
- Option A: Add to initialization code in
app/lib/domain/services/app_initialization.dart - Option B: Let the provider's
build()method handle it naturally (recommended) - Option C: Add to
app/lib/features/geckoview/features/browser/domain/providers.dartinitialization
Recommended Approach: Let the Riverpod provider handle it. When UI first renders, it will:
- Read from database via
generalSettingsRepositoryProvider - Provider's
build()callsgetUseExternalDownloadManager()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:
# 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
-
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.
-
Database as Storage: Setting is stored in the database (user.db, setting table) following the same pattern as other GeneralSettings.
-
Provider Architecture: Uses
@Riverpod(keepAlive: true)to ensure the provider persists across widget rebuilds. -
Async Provider: The provider returns
Future<bool>because native Pigeon calls are asynchronous. -
Auto-Dispose Prevention:
keepAlive: trueensures 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
ANYtype for flexible value storage
DAO Pattern
File: app/lib/features/user/data/database/daos/setting.dart:29-62
updateSetting()methodgetAllSettingsOfPartitionKey()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:
- User can toggle "Use external download manager" in Privacy & Security settings
- Toggle state persists in database (user.db, setting table)
- When enabled, clicking download links prompts to open in external apps
- When disabled, downloads use built-in download manager
- Setting syncs correctly between Flutter and native layers
- No SharedPreferences are used for this feature (verified requirement)
- Code follows existing WebLibre patterns (GeneralSettings, AppLinks, etc.)