diff --git a/lib/main.dart b/lib/main.dart index 6272441c2..452264f7c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,8 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/routes.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; @@ -41,7 +43,9 @@ class AvesApp extends StatefulWidget { class _AvesAppState extends State { Future _appSetup; - final NavigatorObserver _routeTracker = CrashlyticsRouteTracker(); + // observers are not registered when using the same list object with different items + // the list itself needs to be reassigned + List _navigatorObservers = []; final _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); final _navigatorKey = GlobalKey(); @@ -93,11 +97,12 @@ class _AvesAppState extends State { Future _setup() async { await Firebase.initializeApp().then((app) { - FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; - FirebaseCrashlytics.instance.setCustomKey('locales', window.locales.join(', ')); + final crashlytics = FirebaseCrashlytics.instance; + FlutterError.onError = crashlytics.recordFlutterError; + crashlytics.setCustomKey('locales', window.locales.join(', ')); final now = DateTime.now(); - FirebaseCrashlytics.instance.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); - FirebaseCrashlytics.instance.setCustomKey( + crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); + crashlytics.setCustomKey( 'build_mode', kReleaseMode ? 'release' @@ -106,7 +111,11 @@ class _AvesAppState extends State { : 'debug'); }); await settings.init(); - await settings.initCrashlytics(); + await settings.initFirebase(); + _navigatorObservers = [ + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()), + CrashlyticsRouteTracker(), + ]; } void _onNewIntent(Map intentData) { @@ -126,28 +135,20 @@ class _AvesAppState extends State { Widget build(BuildContext context) { // place the settings provider above `MaterialApp` // so it can be used during navigation transitions - final home = FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) { - return getFirstPage(); - } - return Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), - ); - }, - ); return SettingsProvider( child: OverlaySupport( child: FutureBuilder( future: _appSetup, builder: (context, snapshot) { + final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(), + ); return MaterialApp( navigatorKey: _navigatorKey, home: home, - navigatorObservers: [ - if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) _routeTracker, - ], + navigatorObservers: _navigatorObservers, title: 'Aves', darkTheme: darkTheme, themeMode: ThemeMode.dark, diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 846d3206a..05e64142a 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -3,6 +3,7 @@ import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; @@ -57,9 +58,14 @@ class Settings extends ChangeNotifier { // Crashlytics initialization is separated from the main settings initialization // to allow settings customization without Firebase context (e.g. before a Flutter Driver test) - Future initCrashlytics() async { + Future initFirebase() async { await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled); await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled); + await FirebaseAnalytics().setAnalyticsCollectionEnabled(isCrashlyticsEnabled); + // enable analytics debug mode: + // # %ANDROID_SDK%/platform-tools/adb shell setprop debug.firebase.analytics.app deckers.thibault.aves.debug + // disable analytics debug mode: + // # %ANDROID_SDK%/platform-tools/adb shell setprop debug.firebase.analytics.app .none. } Future reset() { @@ -76,7 +82,7 @@ class Settings extends ChangeNotifier { set isCrashlyticsEnabled(bool newValue) { setAndNotify(isCrashlyticsEnabledKey, newValue); - unawaited(initCrashlytics()); + unawaited(initFirebase()); } bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true); diff --git a/lib/widgets/app_debug_page.dart b/lib/widgets/app_debug_page.dart index b90f2cf65..3c61a6fa9 100644 --- a/lib/widgets/app_debug_page.dart +++ b/lib/widgets/app_debug_page.dart @@ -13,6 +13,7 @@ import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; @@ -109,6 +110,21 @@ class AppDebugPageState extends State { ), ], ), + Row( + children: [ + Expanded( + child: Text('Analytics'), + ), + SizedBox(width: 8), + ElevatedButton( + onPressed: () => FirebaseAnalytics().logEvent( + name: 'debug_test', + parameters: {'time': DateTime.now().toIso8601String()}, + ), + child: Text('Send event'), + ), + ], + ), Text('Firebase data collection: ${Firebase.app().isAutomaticDataCollectionEnabled ? 'enabled' : 'disabled'}'), Text('Crashlytics collection: ${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled ? 'enabled' : 'disabled'}'), Divider(), diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index 46188e218..f10dd760b 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -6,8 +6,10 @@ import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:pedantic/pedantic.dart'; class MediaStoreSource extends CollectionSource { Future init() async { @@ -68,16 +70,31 @@ class MediaStoreSource extends CollectionSource { onDone: () async { addPendingEntries(); debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}'); + await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries updateAlbums(); + final analytics = FirebaseAnalytics(); + unawaited(analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(rawEntries.length, 3)).toString())); + unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(sortedAlbums.length, 1)).toString())); + stateNotifier.value = SourceState.cataloguing; await catalogEntries(); + unawaited(analytics.setUserProperty(name: 'tag_count', value: (ceilBy(sortedTags.length, 1)).toString())); + stateNotifier.value = SourceState.locating; await locateEntries(); + unawaited(analytics.setUserProperty(name: 'country_count', value: (ceilBy(sortedCountries.length, 1)).toString())); + stateNotifier.value = SourceState.ready; debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}'); }, onError: (error) => debugPrint('$runtimeType stream error=$error'), ); } + + // e.g. x=12345, precision=3 should return 13000 + int ceilBy(num x, int precision) { + final factor = pow(10, precision); + return (x / factor).ceil() * factor; + } } diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 974f18018..29911ebd0 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -46,7 +46,7 @@ abstract class ChipSetActionDelegate { options: { ChipSortFactor.date: 'By date', ChipSortFactor.name: 'By name', - ChipSortFactor.count: 'By entry count', + ChipSortFactor.count: 'By item count', }, title: 'Sort', ), diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index bf6358660..00936493d 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -115,7 +115,7 @@ class SettingsPage extends StatelessWidget { SwitchListTile( value: settings.isCrashlyticsEnabled, onChanged: (v) => settings.isCrashlyticsEnabled = v, - title: Text('Allow anonymous crash reporting'), + title: Text('Allow anonymous analytics and crash reporting'), ), GrantedDirectories(), ], diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index bac3b04e5..fb254a531 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -99,7 +99,7 @@ class _WelcomePageState extends State { LabeledCheckbox( value: settings.isCrashlyticsEnabled, onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v), - text: 'Allow anonymous crash reporting', + text: 'Allow anonymous analytics and crash reporting', ), LabeledCheckbox( key: Key('agree-checkbox'), diff --git a/pubspec.lock b/pubspec.lock index e04527389..5b1de15c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "7.3.2" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.2" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" firebase_core: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 3ce0d8a31..6c2cd36a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: git: url: git://github.com/deckerst/expansion_tile_card.git firebase_core: + firebase_analytics: firebase_crashlytics: flushbar: flutter_ijkplayer: