diff --git a/docs/external_download_manager_implementation_plan.md b/docs/external_download_manager_implementation_plan.md deleted file mode 100644 index 08487fe2..00000000 --- a/docs/external_download_manager_implementation_plan.md +++ /dev/null @@ -1,379 +0,0 @@ -# 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.)