support for external download managers
This commit is contained in:
+17
@@ -99,6 +99,23 @@ class EngineSettingsReplicationService
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.useExternalDownloadManager,
|
||||
),
|
||||
(previous, next) async {
|
||||
await _service.setUseExternalDownloadManager(next);
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Error listening to useExternalDownloadManager',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
fireImmediately: true,
|
||||
engineSettingsRepositoryProvider,
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
|
||||
}
|
||||
|
||||
String _$engineSettingsReplicationServiceHash() =>
|
||||
r'3e06f0c273090f9f9dfc42f2140d60ef9557f176';
|
||||
r'7a4db374b521e592847a85cb8d2aa184be788ee4';
|
||||
|
||||
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -49,6 +49,7 @@ class TabsBehaviorSettingsScreen extends StatelessWidget {
|
||||
_TabCreationSection(),
|
||||
_TabOrganizationSection(),
|
||||
_TabInteractionSection(),
|
||||
_DownloadsSection(),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -404,3 +405,45 @@ class _AppLinksModeSection extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DownloadsSection extends StatelessWidget {
|
||||
const _DownloadsSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
children: [
|
||||
SettingSection(name: 'Downloads'),
|
||||
_ExternalDownloadManagerTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExternalDownloadManagerTile extends HookConsumerWidget {
|
||||
const _ExternalDownloadManagerTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final useExternalDownloadManager = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(s) => s.useExternalDownloadManager,
|
||||
),
|
||||
);
|
||||
|
||||
return SwitchListTile.adaptive(
|
||||
title: const Text('Use external download manager'),
|
||||
subtitle: const Text('Manage downloads with another app'),
|
||||
secondary: const Icon(MdiIcons.download),
|
||||
value: useExternalDownloadManager,
|
||||
onChanged: (value) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save(
|
||||
(currentSettings) =>
|
||||
currentSettings.copyWith.useExternalDownloadManager(value),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ class GeneralSettings with FastEquatable {
|
||||
final TabBarPosition tabBarPosition;
|
||||
final QuickTabSwitcherMode quickTabSwitcherMode;
|
||||
final bool pullToRefreshEnabled;
|
||||
final bool useExternalDownloadManager;
|
||||
final bool doubleBackCloseTab;
|
||||
final Duration unassignedTabsAutoCleanInterval;
|
||||
final int maxSearchHistoryEntries;
|
||||
@@ -107,6 +108,7 @@ class GeneralSettings with FastEquatable {
|
||||
required this.tabBarPosition,
|
||||
required this.quickTabSwitcherMode,
|
||||
required this.pullToRefreshEnabled,
|
||||
required this.useExternalDownloadManager,
|
||||
required this.doubleBackCloseTab,
|
||||
required this.unassignedTabsAutoCleanInterval,
|
||||
required this.maxSearchHistoryEntries,
|
||||
@@ -134,6 +136,7 @@ class GeneralSettings with FastEquatable {
|
||||
TabBarPosition? tabBarPosition,
|
||||
QuickTabSwitcherMode? quickTabSwitcherMode,
|
||||
bool? pullToRefreshEnabled,
|
||||
bool? useExternalDownloadManager,
|
||||
bool? doubleBackCloseTab,
|
||||
Duration? unassignedTabsAutoCleanInterval,
|
||||
int? maxSearchHistoryEntries,
|
||||
@@ -161,6 +164,7 @@ class GeneralSettings with FastEquatable {
|
||||
quickTabSwitcherMode =
|
||||
quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs,
|
||||
pullToRefreshEnabled = pullToRefreshEnabled ?? true,
|
||||
useExternalDownloadManager = useExternalDownloadManager ?? false,
|
||||
doubleBackCloseTab = doubleBackCloseTab ?? true,
|
||||
unassignedTabsAutoCleanInterval =
|
||||
unassignedTabsAutoCleanInterval ?? Duration.zero,
|
||||
@@ -194,6 +198,7 @@ class GeneralSettings with FastEquatable {
|
||||
tabBarPosition,
|
||||
quickTabSwitcherMode,
|
||||
pullToRefreshEnabled,
|
||||
useExternalDownloadManager,
|
||||
doubleBackCloseTab,
|
||||
unassignedTabsAutoCleanInterval,
|
||||
maxSearchHistoryEntries,
|
||||
|
||||
@@ -59,6 +59,8 @@ abstract class _$GeneralSettingsCWProxy {
|
||||
|
||||
GeneralSettings pullToRefreshEnabled(bool pullToRefreshEnabled);
|
||||
|
||||
GeneralSettings useExternalDownloadManager(bool useExternalDownloadManager);
|
||||
|
||||
GeneralSettings doubleBackCloseTab(bool doubleBackCloseTab);
|
||||
|
||||
GeneralSettings unassignedTabsAutoCleanInterval(
|
||||
@@ -96,6 +98,7 @@ abstract class _$GeneralSettingsCWProxy {
|
||||
TabBarPosition tabBarPosition,
|
||||
QuickTabSwitcherMode quickTabSwitcherMode,
|
||||
bool pullToRefreshEnabled,
|
||||
bool useExternalDownloadManager,
|
||||
bool doubleBackCloseTab,
|
||||
Duration unassignedTabsAutoCleanInterval,
|
||||
int maxSearchHistoryEntries,
|
||||
@@ -197,6 +200,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
GeneralSettings pullToRefreshEnabled(bool pullToRefreshEnabled) =>
|
||||
call(pullToRefreshEnabled: pullToRefreshEnabled);
|
||||
|
||||
@override
|
||||
GeneralSettings useExternalDownloadManager(bool useExternalDownloadManager) =>
|
||||
call(useExternalDownloadManager: useExternalDownloadManager);
|
||||
|
||||
@override
|
||||
GeneralSettings doubleBackCloseTab(bool doubleBackCloseTab) =>
|
||||
call(doubleBackCloseTab: doubleBackCloseTab);
|
||||
@@ -240,6 +247,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
Object? tabBarPosition = const $CopyWithPlaceholder(),
|
||||
Object? quickTabSwitcherMode = const $CopyWithPlaceholder(),
|
||||
Object? pullToRefreshEnabled = const $CopyWithPlaceholder(),
|
||||
Object? useExternalDownloadManager = const $CopyWithPlaceholder(),
|
||||
Object? doubleBackCloseTab = const $CopyWithPlaceholder(),
|
||||
Object? unassignedTabsAutoCleanInterval = const $CopyWithPlaceholder(),
|
||||
Object? maxSearchHistoryEntries = const $CopyWithPlaceholder(),
|
||||
@@ -367,6 +375,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
|
||||
? _value.pullToRefreshEnabled
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: pullToRefreshEnabled as bool,
|
||||
useExternalDownloadManager:
|
||||
useExternalDownloadManager == const $CopyWithPlaceholder() ||
|
||||
useExternalDownloadManager == null
|
||||
? _value.useExternalDownloadManager
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: useExternalDownloadManager as bool,
|
||||
doubleBackCloseTab:
|
||||
doubleBackCloseTab == const $CopyWithPlaceholder() ||
|
||||
doubleBackCloseTab == null
|
||||
@@ -450,6 +464,7 @@ GeneralSettings _$GeneralSettingsFromJson(
|
||||
json['quickTabSwitcherMode'],
|
||||
),
|
||||
pullToRefreshEnabled: json['pullToRefreshEnabled'] as bool?,
|
||||
useExternalDownloadManager: json['useExternalDownloadManager'] as bool?,
|
||||
doubleBackCloseTab: json['doubleBackCloseTab'] as bool?,
|
||||
unassignedTabsAutoCleanInterval:
|
||||
json['unassignedTabsAutoCleanInterval'] == null
|
||||
@@ -493,6 +508,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
||||
'quickTabSwitcherMode':
|
||||
_$QuickTabSwitcherModeEnumMap[instance.quickTabSwitcherMode]!,
|
||||
'pullToRefreshEnabled': instance.pullToRefreshEnabled,
|
||||
'useExternalDownloadManager': instance.useExternalDownloadManager,
|
||||
'doubleBackCloseTab': instance.doubleBackCloseTab,
|
||||
'unassignedTabsAutoCleanInterval':
|
||||
instance.unassignedTabsAutoCleanInterval.inMicroseconds,
|
||||
|
||||
@@ -126,6 +126,8 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
|
||||
DriftSqlType.bool,
|
||||
db.typeMapping,
|
||||
),
|
||||
'useExternalDownloadManager': settings['useExternalDownloadManager']
|
||||
?.readAs(DriftSqlType.bool, db.typeMapping),
|
||||
'doubleBackCloseTab': settings['doubleBackCloseTab']?.readAs(
|
||||
DriftSqlType.bool,
|
||||
db.typeMapping,
|
||||
|
||||
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
|
||||
}
|
||||
|
||||
String _$generalSettingsRepositoryHash() =>
|
||||
r'0d4b01ff15942b513a7cdceee7bf43721ae1c074';
|
||||
r'7cea68308e2e7847e9bb030e9c9c04860c760edd';
|
||||
|
||||
abstract class _$GeneralSettingsRepository
|
||||
extends $StreamNotifier<GeneralSettings> {
|
||||
|
||||
@@ -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.)
|
||||
+3
@@ -287,6 +287,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
requestDownloadPermissionsLauncher.launch(permissions)
|
||||
},
|
||||
shouldForwardToThirdParties = {
|
||||
GlobalComponents.useExternalDownloadManager
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
|
||||
+3
@@ -57,6 +57,9 @@ object GlobalComponents {
|
||||
// Engine settings API for managing engine-specific settings
|
||||
var engineSettingsApi: GeckoEngineSettingsApiImpl? = null
|
||||
|
||||
// External download manager setting
|
||||
var useExternalDownloadManager: Boolean = false
|
||||
|
||||
fun shouldOpenLinksInApp(): Boolean {
|
||||
return when (engineSettingsApi!!.getAppLinksMode()) {
|
||||
eu.weblibre.flutter_mozilla_components.pigeons.AppLinksMode.ALWAYS -> true
|
||||
|
||||
+8
@@ -350,4 +350,12 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
|
||||
else -> AppLinksMode.ASK
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUseExternalDownloadManager(enabled: Boolean) {
|
||||
GlobalComponents.useExternalDownloadManager = enabled
|
||||
}
|
||||
|
||||
override fun getUseExternalDownloadManager(): Boolean {
|
||||
return GlobalComponents.useExternalDownloadManager
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -3850,6 +3850,12 @@ interface GeckoEngineSettingsApi {
|
||||
*/
|
||||
fun setAppLinksMode(mode: AppLinksMode)
|
||||
fun getAppLinksMode(): AppLinksMode
|
||||
/**
|
||||
* Sets whether to use external download managers for downloads.
|
||||
* When enabled, downloads are forwarded to third-party apps like ADM, 1DM, AB DM.
|
||||
*/
|
||||
fun setUseExternalDownloadManager(enabled: Boolean)
|
||||
fun getUseExternalDownloadManager(): Boolean
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoEngineSettingsApi. */
|
||||
@@ -3947,6 +3953,39 @@ interface GeckoEngineSettingsApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setUseExternalDownloadManager$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val enabledArg = args[0] as Boolean
|
||||
val wrapped: List<Any?> = try {
|
||||
api.setUseExternalDownloadManager(enabledArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getUseExternalDownloadManager$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getUseExternalDownloadManager())
|
||||
} catch (exception: Throwable) {
|
||||
GeckoPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -148,4 +148,14 @@ class GeckoEngineSettingsService {
|
||||
Future<AppLinksMode> getAppLinksMode() {
|
||||
return _api.getAppLinksMode();
|
||||
}
|
||||
|
||||
/// Sets whether to use external download managers for downloads.
|
||||
/// When enabled, downloads are forwarded to third-party apps like ADM, 1DM, AB DM.
|
||||
Future<void> setUseExternalDownloadManager(bool enabled) {
|
||||
return _api.setUseExternalDownloadManager(enabled);
|
||||
}
|
||||
|
||||
Future<bool> getUseExternalDownloadManager() {
|
||||
return _api.getUseExternalDownloadManager();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4481,6 +4481,57 @@ class GeckoEngineSettingsApi {
|
||||
return (pigeonVar_replyList[0] as AppLinksMode?)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets whether to use external download managers for downloads.
|
||||
/// When enabled, downloads are forwarded to third-party apps like ADM, 1DM, AB DM.
|
||||
Future<void> setUseExternalDownloadManager(bool enabled) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.setUseExternalDownloadManager$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[enabled]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> getUseExternalDownloadManager() async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoEngineSettingsApi.getUseExternalDownloadManager$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
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?)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeckoSessionApi {
|
||||
|
||||
@@ -1012,6 +1012,12 @@ abstract class GeckoEngineSettingsApi {
|
||||
void setAppLinksMode(AppLinksMode mode);
|
||||
|
||||
AppLinksMode getAppLinksMode();
|
||||
|
||||
/// Sets whether to use external download managers for downloads.
|
||||
/// When enabled, downloads are forwarded to third-party apps like ADM, 1DM, AB DM.
|
||||
void setUseExternalDownloadManager(bool enabled);
|
||||
|
||||
bool getUseExternalDownloadManager();
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
|
||||
Reference in New Issue
Block a user