support for external download managers
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
# 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<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:
|
||||
```dart
|
||||
@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:
|
||||
```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<bool>` 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.)
|
||||
Reference in New Issue
Block a user