decoupled services from settings init

This commit is contained in:
Thibault Deckers 2021-09-24 16:19:18 +09:00
parent f5a8d6c90d
commit 8f2a0a8247
16 changed files with 120 additions and 144 deletions

View file

@ -83,6 +83,6 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
companion object {
private val LOG_TAG = LogUtils.createTag<SettingsChangeStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/settingschange"
const val CHANNEL = "deckers.thibault/aves/settings_change"
}
}

View file

@ -746,8 +746,8 @@
"settingsSectionPrivacy": "Privacy",
"@settingsSectionPrivacy": {},
"settingsEnableCrashReport": "Allow anonymous error reporting",
"@settingsEnableCrashReport": {},
"settingsEnableErrorReporting": "Allow anonymous error reporting",
"@settingsEnableErrorReporting": {},
"settingsSaveSearchHistory": "Save search history",
"@settingsSaveSearchHistory": {},

View file

@ -364,7 +364,7 @@
"settingsSubtitleThemeTextAlignmentRight": "오른쪽",
"settingsSectionPrivacy": "개인정보 보호",
"settingsEnableCrashReport": "오류 보고서 보내기",
"settingsEnableErrorReporting": "오류 보고서 보내기",
"settingsSaveSearchHistory": "검색기록",
"settingsHiddenFiltersTile": "숨겨진 필터",

View file

@ -13,7 +13,7 @@ import 'package:flutter/material.dart';
class SettingsDefaults {
// app
static const hasAcceptedTerms = false;
static const isCrashlyticsEnabled = false;
static const isErrorReportingEnabled = false;
static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection;

View file

@ -9,7 +9,6 @@ import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/map_style.dart';
import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
@ -20,7 +19,10 @@ import 'package:shared_preferences/shared_preferences.dart';
final Settings settings = Settings._private();
class Settings extends ChangeNotifier {
final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settingschange');
final EventChannel _platformSettingsChangeChannel = const EventChannel('deckers.thibault/aves/settings_change');
final StreamController<String> _updateStreamController = StreamController<String>.broadcast();
Stream<String> get updateStream => _updateStreamController.stream;
static SharedPreferences? _prefs;
@ -38,7 +40,7 @@ class Settings extends ChangeNotifier {
// app
static const hasAcceptedTermsKey = 'has_accepted_terms';
static const isCrashlyticsEnabledKey = 'is_crashlytics_enabled';
static const isErrorReportingEnabledKey = 'is_crashlytics_enabled';
static const localeKey = 'locale';
static const mustBackTwiceToExitKey = 'must_back_twice_to_exit';
static const keepScreenOnKey = 'keep_screen_on';
@ -105,9 +107,13 @@ class Settings extends ChangeNotifier {
// version
static const lastVersionCheckDateKey = 'last_version_check_date';
Future<void> init() async {
// platform settings
// cf Android `Settings.System.ACCELEROMETER_ROTATION`
static const platformAccelerometerRotationKey = 'accelerometer_rotation';
Future<void> init({bool isRotationLocked = false}) async {
_prefs = await SharedPreferences.getInstance();
_isRotationLocked = await windowService.isRotationLocked();
_isRotationLocked = isRotationLocked;
}
Future<void> reset({required bool includeInternalKeys}) async {
@ -139,12 +145,9 @@ class Settings extends ChangeNotifier {
set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue);
bool get isCrashlyticsEnabled => getBoolOrDefault(isCrashlyticsEnabledKey, SettingsDefaults.isCrashlyticsEnabled);
bool get isErrorReportingEnabled => getBoolOrDefault(isErrorReportingEnabledKey, SettingsDefaults.isErrorReportingEnabled);
set isCrashlyticsEnabled(bool newValue) {
setAndNotify(isCrashlyticsEnabledKey, newValue);
unawaited(reportService.setCollectionEnabled(isCrashlyticsEnabled));
}
set isErrorReportingEnabled(bool newValue) => setAndNotify(isErrorReportingEnabledKey, newValue);
static const localeSeparator = '-';
@ -180,10 +183,7 @@ class Settings extends ChangeNotifier {
KeepScreenOn get keepScreenOn => getEnumOrDefault(keepScreenOnKey, SettingsDefaults.keepScreenOn, KeepScreenOn.values);
set keepScreenOn(KeepScreenOn newValue) {
setAndNotify(keepScreenOnKey, newValue.toString());
newValue.apply();
}
set keepScreenOn(KeepScreenOn newValue) => setAndNotify(keepScreenOnKey, newValue.toString());
HomePageSetting get homePage => getEnumOrDefault(homePageKey, SettingsDefaults.homePage, HomePageSetting.values);
@ -418,6 +418,7 @@ class Settings extends ChangeNotifier {
_prefs!.setBool(key, newValue);
}
if (oldValue != newValue) {
_updateStreamController.add(key);
notifyListeners();
}
}
@ -427,8 +428,7 @@ class Settings extends ChangeNotifier {
void _onPlatformSettingsChange(Map? fields) {
fields?.forEach((key, value) {
switch (key) {
// cf Android `Settings.System.ACCELEROMETER_ROTATION`
case 'accelerometer_rotation':
case platformAccelerometerRotationKey:
if (value is int) {
final newValue = value == 0;
if (_isRotationLocked != newValue) {
@ -436,6 +436,7 @@ class Settings extends ChangeNotifier {
if (!_isRotationLocked) {
windowService.requestOrientation();
}
_updateStreamController.add(key);
notifyListeners();
}
}
@ -488,7 +489,7 @@ class Settings extends ChangeNotifier {
debugPrint('failed to import key=$key, value=$value is not a double');
}
break;
case isCrashlyticsEnabledKey:
case isErrorReportingEnabledKey:
case mustBackTwiceToExitKey:
case showThumbnailLocationKey:
case showThumbnailMotionPhotoKey:
@ -545,6 +546,7 @@ class Settings extends ChangeNotifier {
break;
}
}
_updateStreamController.add(key);
});
notifyListeners();
}

View file

@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
import 'package:stack_trace/stack_trace.dart';
abstract class ReportService {
Future<void> init();
bool get isCollectionEnabled;
Future<void> setCollectionEnabled(bool enabled);
@ -22,26 +24,29 @@ abstract class ReportService {
}
class CrashlyticsReportService extends ReportService {
FirebaseCrashlytics get instance => FirebaseCrashlytics.instance;
FirebaseCrashlytics get _instance => FirebaseCrashlytics.instance;
@override
bool get isCollectionEnabled => instance.isCrashlyticsCollectionEnabled;
Future<void> init() => Firebase.initializeApp();
@override
bool get isCollectionEnabled => _instance.isCrashlyticsCollectionEnabled;
@override
Future<void> setCollectionEnabled(bool enabled) async {
debugPrint('${enabled ? 'enable' : 'disable'} Firebase & Crashlytics collection');
await Firebase.app().setAutomaticDataCollectionEnabled(enabled);
await instance.setCrashlyticsCollectionEnabled(enabled);
await _instance.setCrashlyticsCollectionEnabled(enabled);
}
@override
Future<void> log(String message) => instance.log(message);
Future<void> log(String message) => _instance.log(message);
@override
Future<void> setCustomKey(String key, Object value) => instance.setCustomKey(key, value);
Future<void> setCustomKey(String key, Object value) => _instance.setCustomKey(key, value);
@override
Future<void> setCustomKeys(Map<String, Object> map) {
final _instance = instance;
return Future.forEach<MapEntry<String, Object>>(map.entries, (kv) => _instance.setCustomKey(kv.key, kv.value));
}
@ -60,11 +65,11 @@ class CrashlyticsReportService extends ReportService {
)
.join('\n'));
}
return instance.recordError(exception, stack);
return _instance.recordError(exception, stack);
}
@override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) {
return instance.recordFlutterError(flutterErrorDetails);
return _instance.recordFlutterError(flutterErrorDetails);
}
}

View file

@ -1,6 +1,7 @@
import 'dart:ui';
import 'package:aves/app_mode.dart';
import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/media_store_source.dart';
@ -16,7 +17,6 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -123,25 +123,38 @@ class _AvesAppState extends State<AvesApp> {
}
Future<void> _setup() async {
await Firebase.initializeApp().then((app) async {
FlutterError.onError = reportService.recordFlutterError;
final now = DateTime.now();
final hasPlayServices = await availability.hasPlayServices;
await reportService.setCustomKeys({
'build_mode': kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug',
'has_play_services': hasPlayServices,
'locales': window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
});
await settings.init(
isRotationLocked: await windowService.isRotationLocked(),
);
// keep screen on
settings.updateStream.where((key) => key == Settings.keepScreenOnKey).listen(
(_) => settings.keepScreenOn.apply(),
);
settings.keepScreenOn.apply();
// error reporting
await reportService.init();
settings.updateStream.where((key) => key == Settings.isErrorReportingEnabledKey).listen(
(_) => reportService.setCollectionEnabled(settings.isErrorReportingEnabled),
);
await reportService.setCollectionEnabled(settings.isErrorReportingEnabled);
FlutterError.onError = reportService.recordFlutterError;
final now = DateTime.now();
final hasPlayServices = await availability.hasPlayServices;
await reportService.setCustomKeys({
'build_mode': kReleaseMode
? 'release'
: kProfileMode
? 'profile'
: 'debug',
'has_play_services': hasPlayServices,
'locales': window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
});
await settings.init();
await reportService.setCollectionEnabled(settings.isCrashlyticsEnabled);
_navigatorObservers = [
CrashlyticsRouteTracker(),
ReportingRouteTracker(),
];
}

View file

@ -1,7 +1,7 @@
import 'package:aves/services/common/services.dart';
import 'package:flutter/material.dart';
class CrashlyticsRouteTracker extends NavigatorObserver {
class ReportingRouteTracker extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => reportService.log('Nav didPush to ${_name(route)}');

View file

@ -4,12 +4,11 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/home_page.dart';
import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/global_search.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/global_search.dart';
import 'package:aves/services/viewer_service.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/collection/collection_page.dart';
@ -50,7 +49,6 @@ class _HomePageState extends State<HomePage> {
super.initState();
_setup();
imageCache!.maximumSizeBytes = 512 * (1 << 20);
settings.keepScreenOn.apply();
}
@override

View file

@ -20,7 +20,7 @@ class PrivacySection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final currentIsCrashlyticsEnabled = context.select<Settings, bool>((s) => s.isCrashlyticsEnabled);
final currentIsErrorReportingEnabled = context.select<Settings, bool>((s) => s.isErrorReportingEnabled);
final currentSaveSearchHistory = context.select<Settings, bool>((s) => s.saveSearchHistory);
return AvesExpansionTile(
@ -33,9 +33,9 @@ class PrivacySection extends StatelessWidget {
showHighlight: false,
children: [
SwitchListTile(
value: currentIsCrashlyticsEnabled,
onChanged: (v) => settings.isCrashlyticsEnabled = v,
title: Text(context.l10n.settingsEnableCrashReport),
value: currentIsErrorReportingEnabled,
onChanged: (v) => settings.isErrorReportingEnabled = v,
title: Text(context.l10n.settingsEnableErrorReporting),
),
SwitchListTile(
value: currentSaveSearchHistory,

View file

@ -102,9 +102,9 @@ class _WelcomePageState extends State<WelcomePage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LabeledCheckbox(
value: settings.isCrashlyticsEnabled,
value: settings.isErrorReportingEnabled,
onChanged: (v) {
if (v != null) setState(() => settings.isCrashlyticsEnabled = v);
if (v != null) setState(() => settings.isErrorReportingEnabled = v);
},
text: context.l10n.welcomeCrashReportToggle,
),

View file

@ -117,7 +117,7 @@ flutter:
# Test driver
# run (any device):
# % flutter drive -t test_driver/app.dart --profile
# % flutter drive -t test_driver/driver_app.dart --profile
# capture shaders in profile mode (real device only):
# % flutter drive -t test_driver/app.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json
# % flutter drive -t test_driver/driver_app.dart --profile --cache-sksl --write-sksl-on-exit shaders.sksl.json

View file

@ -3,6 +3,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
class FakeReportService extends ReportService {
@override
Future<void> init() => SynchronousFuture(null);
@override
bool get isCollectionEnabled => false;

View file

@ -1,84 +0,0 @@
import 'dart:ui';
import 'package:aves/main.dart' as app;
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:aves/services/report_service.dart';
import 'package:aves/services/window_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'constants.dart';
void main() {
enableFlutterDriverExtension();
// scan files copied from test assets
// we do it via the app instead of broadcasting via ADB
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
PlatformMediaStoreService()
..scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml')
..scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
// something like `configure().then((_) => app.main());` does not behave as expected
// and starts the app without waiting for `configure` to complete
configureAndLaunch();
}
Future<void> configureAndLaunch() async {
// TODO TLAD [test] decouple services from settings setters, so there is no need for fake services here
// set up fake services called during settings initialization
getIt
..registerSingleton<WindowService>(DriverInitWindowService())
..registerSingleton<ReportService>(DriverInitReportService());
await settings.init();
settings
..keepScreenOn = KeepScreenOn.always
..hasAcceptedTerms = false
..isCrashlyticsEnabled = false
..locale = const Locale('en')
..homePage = HomePageSetting.collection
..imageBackground = EntryBackground.checkered;
// tear down fake services
await Future.delayed(const Duration(seconds: 1));
await getIt.reset();
app.main();
}
class DriverInitWindowService extends Fake implements WindowService {
@override
Future<void> keepScreenOn(bool on) => SynchronousFuture(null);
@override
Future<bool> isRotationLocked() => SynchronousFuture(false);
}
class DriverInitReportService extends Fake implements ReportService {
@override
bool get isCollectionEnabled => false;
@override
Future<void> setCollectionEnabled(bool enabled) => SynchronousFuture(null);
@override
Future<void> log(String message) => SynchronousFuture(null);
@override
Future<void> setCustomKey(String key, Object value) => SynchronousFuture(null);
@override
Future<void> setCustomKeys(Map<String, Object> map) => SynchronousFuture(null);
@override
Future<void> recordError(exception, StackTrace? stack) => SynchronousFuture(null);
@override
Future<void> recordFlutterError(FlutterErrorDetails flutterErrorDetails) => SynchronousFuture(null);
}

View file

@ -0,0 +1,39 @@
import 'dart:ui';
import 'package:aves/main.dart' as app;
import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/media/media_store_service.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'constants.dart';
void main() {
enableFlutterDriverExtension();
// scan files copied from test assets
// we do it via the app instead of broadcasting via ADB
// because `MEDIA_SCANNER_SCAN_FILE` intent got deprecated in API 29
PlatformMediaStoreService()
..scanFile(p.join(targetPicturesDir, 'aves_logo.svg'), 'image/svg+xml')
..scanFile(p.join(targetPicturesDir, 'ipse.jpg'), 'image/jpeg');
// something like `configure().then((_) => app.main());` does not behave as expected
// and starts the app without waiting for `configure` to complete
configureAndLaunch();
}
Future<void> configureAndLaunch() async {
await settings.init();
settings
..keepScreenOn = KeepScreenOn.always
..hasAcceptedTerms = false
..isErrorReportingEnabled = false
..locale = const Locale('en')
..homePage = HomePageSetting.collection
..imageBackground = EntryBackground.checkered;
app.main();
}