diff --git a/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_migrations.dart b/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_migrations.dart new file mode 100644 index 00000000..57953c37 --- /dev/null +++ b/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_migrations.dart @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024-2026 Fabian Freund. + * + * This file is part of WebLibre + * (see https://weblibre.eu). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +typedef PreferenceMigrationStep = Future Function(); + +class PreferenceMigrationRunner { + const PreferenceMigrationRunner({ + required this.schemaVersion, + required this.steps, + }); + + final int schemaVersion; + final Map steps; + + Future run({ + required Future Function() readVersion, + required Future Function(int version) writeVersion, + }) async { + var version = await readVersion(); + + while (version < schemaVersion) { + final step = steps[version]; + if (step == null) { + throw StateError( + 'Missing preference migration from version ' + '$version to ${version + 1}', + ); + } + + await step(); + version++; + await writeVersion(version); + } + + return version; + } +} diff --git a/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart b/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart index fb5063e1..46449a88 100644 --- a/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart +++ b/apps/weblibre/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart @@ -21,20 +21,83 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; import 'package:weblibre/core/logger.dart'; +import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_migrations.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/models/preference_setting.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/setting_groups_serializer.dart'; +import 'package:weblibre/features/user/data/providers.dart'; part 'preference_settings.g.dart'; +const _preferenceMigrationVersionKey = 'preferenceMigrationVersion'; +const _preferenceMigrationPartitionKey = 'preferenceMigration'; +const _preferenceMigrationSchemaVersion = 1; +const _mainProcessDisableJitPref = + 'javascript.options.main_process_disable_jit'; + @Riverpod(keepAlive: true) class StartupPreferenceEnforcementService extends _$StartupPreferenceEnforcementService { + final _prefManager = GeckoPrefService(); + + PreferenceMigrationRunner get _migrationRunner => PreferenceMigrationRunner( + schemaVersion: _preferenceMigrationSchemaVersion, + steps: { + 0: () => _prefManager.resetPrefs([_mainProcessDisableJitPref]), + }, + ); + + Future _readPreferenceMigrationVersion() async { + final db = ref.read(userDatabaseProvider); + final value = await db.settingDao.getSettingValue( + _preferenceMigrationVersionKey, + ); + + return value?.readAs(DriftSqlType.int, db.typeMapping) ?? 0; + } + + Future _writePreferenceMigrationVersion(int version) async { + await ref + .read(userDatabaseProvider) + .settingDao + .updateSetting( + _preferenceMigrationVersionKey, + _preferenceMigrationPartitionKey, + version, + ); + } + + Future _runMigrations() async { + final previousVersion = await _readPreferenceMigrationVersion(); + final currentVersion = await _migrationRunner.run( + readVersion: _readPreferenceMigrationVersion, + writeVersion: _writePreferenceMigrationVersion, + ); + + if (currentVersion > previousVersion) { + logger.i( + 'Applied preference migrations ' + '$previousVersion -> $currentVersion', + ); + } + } + Future apply() async { + try { + await _runMigrations(); + } catch (e, s) { + logger.w( + 'Failed to run preference migrations, will retry on next launch', + error: e, + stackTrace: s, + ); + } + final content = await ref.read(_preferenceSettingContentProvider.future); final groups = deserializePreferenceSettingGroups( PreferencePartition.user, @@ -58,7 +121,7 @@ class StartupPreferenceEnforcementService return; } - final currentPrefs = await GeckoPrefService().getPrefs( + final currentPrefs = await _prefManager.getPrefs( startupPrefs.keys.toList(), ); @@ -77,7 +140,7 @@ class StartupPreferenceEnforcementService if (prefsToEnforce.isNotEmpty) { try { - await GeckoPrefService().applyPrefs(prefsToEnforce); + await _prefManager.applyPrefs(prefsToEnforce); } catch (e) { logger.w( 'Failed to enforce startup preferences, will retry on next launch', diff --git a/apps/weblibre/lib/features/user/data/database/daos/setting.dart b/apps/weblibre/lib/features/user/data/database/daos/setting.dart index 6f4b5709..04dace69 100644 --- a/apps/weblibre/lib/features/user/data/database/daos/setting.dart +++ b/apps/weblibre/lib/features/user/data/database/daos/setting.dart @@ -29,6 +29,11 @@ import 'package:weblibre/features/user/data/database/definitions.drift.dart'; class SettingDao extends DatabaseAccessor with $SettingDaoMixin { SettingDao(super.attachedDatabase); + Future getSettingValue(String key) async { + final query = db.setting.select()..where((r) => r.key.equals(key)); + return (await query.getSingleOrNull())?.value; + } + Future updateSetting(String key, String? partitionKey, Object? value) { final normalizedValue = (value is Iterable || value is Map) ? jsonEncode(value)