diff --git a/lib/logger/android_logger.dart b/lib/logger/android_logger.dart index 6b59135..fe6a0aa 100644 --- a/lib/logger/android_logger.dart +++ b/lib/logger/android_logger.dart @@ -21,6 +21,7 @@ class AndroidLogger extends SentenceLogger { @override Future startLogger(TfliteRequest request) async { await super.startLogger(request); + // Foreground task initialization to keep app running in background FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'sentence_logger', @@ -52,8 +53,10 @@ class AndroidLogger extends SentenceLogger { } Future startAccessibility() async { + // check for notification permission and request if not granted var statusNotify = await Permission.notification.request(); if (statusNotify.isGranted) { + // Start notification service to keep app running in background FlutterForegroundTask.startService( notificationTitle: "Sentiment Tracker", notificationText: "Analyzing sentence sentiment"); @@ -65,17 +68,21 @@ class AndroidLogger extends SentenceLogger { await FlutterAccessibilityService.requestAccessibilityPermission(); } if (status) { + // if permission granted, start listening to accessibility events FlutterAccessibilityService.accessStream.listen(_accessibilityListener); } } static void _accessibilityListener(AccessibilityEvent event) { + // if the package is blacklisted, ignore the event if (AndroidLogger().blacklist.hasMatch(event.packageName!)) { return; } + // if the event is a window state change, update the foreground app in use if (event.eventType == EventType.typeWindowStateChanged) { event.packageTitle().then((title) { AndroidLogger().updateFGApp(title!); + // log the app icon if it is not already logged if (!AndroidLogger().hasAppIcon(title)) { DeviceApps.getApp(event.packageName!, true).then((app) { var appWIcon = (app as ApplicationWithIcon?)!; @@ -85,8 +92,12 @@ class AndroidLogger extends SentenceLogger { }); return; } + // if the event is a text change, update the current sentence being typed var textNow = event.nodesText![0]; log(textNow); + // if the sentence is a single character (the minimum allowed to be logged + // by android accessibility, and hence when it's most likely a user started + // a new sentence), log the app in use if (textNow.length == 1) { AndroidLogger().addAppEntry(); } @@ -96,6 +107,8 @@ class AndroidLogger extends SentenceLogger { } } +// This extension is used to get the colloquial name of the app that is in use +// instead of the package name for better display in the UI extension NameConversion on AccessibilityEvent { Future packageTitle() async { if (packageName != null) { diff --git a/lib/logger/logger.dart b/lib/logger/logger.dart index ac915c0..a2c574f 100644 --- a/lib/logger/logger.dart +++ b/lib/logger/logger.dart @@ -13,31 +13,38 @@ import '../sentiment_analysis.dart'; class AppList { DateTime lastTimeUsed; double totalTimeUsed; - int numPositive = 0; - int numNegative = 0; + int numPositive = 0; // number of positive sentences + int numNegative = 0; // number of negative sentences AppList(this.lastTimeUsed, this.totalTimeUsed, this.numPositive, this.numNegative); } class SentenceLogger { + // Singleton static final SentenceLogger _instance = SentenceLogger.init(); + // sentence buffer for the current sentence being typed static final StringBuffer _sentence = StringBuffer(); static final HashMap _appMap = HashMap(); static late final HashSet _appIcons; static late DriftIsolate _iso; static late TfParams _tfp; - static const int _updateFreq = 1; //update db every 5 minutes + static const int _updateFreq = 1; //update db every 1 minute + // default app blacklist RegExp blacklist = RegExp(r".*system.*|.*keyboard.*|.*input.*|.*honeyboard.*|.*swiftkey.*"); + // last app used String _lastUsedApp = ""; + // has the db just been updated? (to prevent race conditions) bool _dbUpdated = false; + // Singleton factory SentenceLogger() { return _instance; } SentenceLogger.init(); + // Save the current sentiment logs to the database void logToDB() { Isolate.spawn(SentimentDB.addSentiments, AddSentimentRequest(_appMap, _iso.connectPort)); @@ -57,8 +64,10 @@ class SentenceLogger { request.sendDriftIsolate.send(driftIsolate); } + // Entry point for the background isolate Future startLogger(TfliteRequest request) async { var rPort = ReceivePort(); + // start the drift database in a background isolate await Isolate.spawn( _startBackground, IsolateStartRequest( @@ -66,6 +75,7 @@ class SentenceLogger { ); var prefs = request.prefs; + // get custom blacklist from preferences if (prefs.getString('blacklist') != null) { blacklist = RegExp(prefs.getString('blacklist')!); } @@ -74,6 +84,7 @@ class SentenceLogger { _tfp = request.tfp; var sdb = SentimentDB.connect(await _iso.connect()); _appIcons = await sdb.getListOfIcons(); + // close connection to database and send the drift isolate back to the main isolate sdb.close(); request.sendDriftIsolate.send(_iso.connectPort); } @@ -111,6 +122,9 @@ class SentenceLogger { } newHour = true; } + // used = false if the app was not used in the last 10 minutes, + // but we still want to update the average score as it still affects the + // user's mood if (used || newHour) { app.lastTimeUsed = now; app.totalTimeUsed = totalTimeUsed; @@ -119,6 +133,7 @@ class SentenceLogger { void addAppEntry() async { log(getSentence()); + // if loggable sentence is too short, clear it and return if (getSentence().length < 6) { clearSentence(); return; @@ -145,6 +160,7 @@ class SentenceLogger { _appMap.putIfAbsent(name, () => AppList(now, 0, 1, 1)); } + //Update database every _updateFreq minutes if (now.minute % _updateFreq == 0 && !_dbUpdated) { logToDB(); _dbUpdated = true; @@ -155,6 +171,7 @@ class SentenceLogger { void updateFGApp(String name) { DateTime now = DateTime.now(); + // ignore apps in blacklist if (blacklist.hasMatch(name.toLowerCase())) { return; } @@ -163,6 +180,7 @@ class SentenceLogger { now.difference(_appMap[name]!.lastTimeUsed).inSeconds.toDouble() / 60; if (name == _lastUsedApp) { if (now.hour != _appMap[name]!.lastTimeUsed.hour) { + // if the next hour has been reached reset the time used if (timeUsedSince / now.minute > 1) { _appMap[name]!.totalTimeUsed = now.minute.toDouble(); } else { diff --git a/lib/logger/logger_factory.dart b/lib/logger/logger_factory.dart index 0dbbfe1..0fe0aeb 100644 --- a/lib/logger/logger_factory.dart +++ b/lib/logger/logger_factory.dart @@ -8,6 +8,7 @@ import 'android_logger.dart'; import 'win_logger.dart'; class LoggerFactory { + // start the logger based on the platform @pragma('vm:entry-point') static Future startLoggerFactory(TfliteRequest request) async { if (Platform.isWindows) { @@ -19,6 +20,7 @@ class LoggerFactory { } } + // return the default blacklist for the logger based on the platform static RegExp getLoggerRegex() { if (Platform.isWindows) { return RegExp( @@ -32,29 +34,32 @@ class LoggerFactory { } } + // return the disclosure text for the logger based on the platform as the play store requires + // a disclosure for the accessibility API, which is not available on Windows static Widget getDisclosureText(BuildContext context) { if (Platform.isAndroid) { return SizedBox( width: double.maxFinite, - height: MediaQuery.of(context).size.height * 0.7, - child: - const Markdown(data: "It is important for you to understand how Negate makes use of your data. Below are the key points on why Negate must use the Accessibility API and what features it uses the Accessibility APi for.\n" - "# Accessibility API\n" - "## Listening for Text Changes\n" - "Negate listens for all text typed into textboxes on your device, features that require this access are:\n" - "* Generation of a positivity score using Artificial Intelligence based on the text received.\n" - "* Usage of the average of these scores over time for:\n" - " * The Hourly Dashboard\n" - " * The Daily Breakdown\n" - " * The Weekly Recommendations\n\n" - "## Listening for Window State Changes\n" - "Negate listens for the current app that is in view for the features of:\n" - "* Seeing which apps have been used in the last 10 minutes that could have affected your mood and hence are factored into the positivity scoring for the current app as well.\n" - "* Getting the amount of time spent in each app and hence how much influence they have had on you in the past hour.\n\n" - "The next page details the app's privacy policy and how all user data is handled.")); + height: MediaQuery.of(context).size.height * 0.7, + child: const Markdown( + data: + "It is important for you to understand how Negate makes use of your data. Below are the key points on why Negate must use the Accessibility API and what features it uses the Accessibility APi for.\n" + "# Accessibility API\n" + "## Listening for Text Changes\n" + "Negate listens for all text typed into textboxes on your device, features that require this access are:\n" + "* Generation of a positivity score using Artificial Intelligence based on the text received.\n" + "* Usage of the average of these scores over time for:\n" + " * The Hourly Dashboard\n" + " * The Daily Breakdown\n" + " * The Weekly Recommendations\n\n" + "## Listening for Window State Changes\n" + "Negate listens for the current app that is in view for the features of:\n" + "* Seeing which apps have been used in the last 10 minutes that could have affected your mood and hence are factored into the positivity scoring for the current app as well.\n" + "* Getting the amount of time spent in each app and hence how much influence they have had on you in the past hour.\n\n" + "The next page details the app's privacy policy and how all user data is handled.")); } else { - return SingleChildScrollView(child: ListBody( - children: const [ + return SingleChildScrollView( + child: ListBody(children: const [ Text('This app tracks the text of messages ' 'and what app is currently in use.'), Text( diff --git a/lib/logger/win_logger.dart b/lib/logger/win_logger.dart index e989c75..331d16a 100644 --- a/lib/logger/win_logger.dart +++ b/lib/logger/win_logger.dart @@ -11,6 +11,7 @@ class WinLogger extends SentenceLogger { static final WinLogger _instance = WinLogger.init(); static int _keyHook = 0; static int _mouseHook = 0; + // used to prevent logging keystrokes when the user is not typing static DateTime _lastLogged = DateTime.now(); factory WinLogger() { @@ -23,6 +24,8 @@ class WinLogger extends SentenceLogger { Future startLogger(TfliteRequest request) async { await super.startLogger(request); _setHook(); + // Keeps the app waiting for messages from the OS so that the hooks can be + // called final msg = calloc(); while (GetMessage(msg, NULL, 0, 0) != 0) { TranslateMessage(msg); @@ -31,6 +34,7 @@ class WinLogger extends SentenceLogger { } static int _hookCallback(int nCode, int wParam, int lParam) { + // Only log keydown events if (nCode == HC_ACTION) { if (wParam == WM_KEYDOWN) { final kbs = Pointer.fromAddress(lParam); @@ -41,6 +45,7 @@ class WinLogger extends SentenceLogger { } static int _mouseCallback(int nCode, int wParam, int lParam) { + // log the app name and icon of whatever window is clicked on if (wParam == WM_LBUTTONDOWN) { var name = WinLogger()._getFGAppName(); WinLogger().updateFGApp(name); @@ -64,16 +69,22 @@ class WinLogger extends SentenceLogger { lowercase = !lowercase; } + // if the user presses enter, add the sentence to the database if (keyStroke == 13) { + // if the user presses enter within 10 seconds of the last time they + // pressed any key, add the sentence to the database, otherwise clear if (_lastLogged.difference(DateTime.now()).inSeconds > 10) { clearSentence(); } else { addAppEntry(); } + // if the user presses backspace, remove the last character from the + // sentence } else if (keyStroke == 8) { var temp = getSentence().substring(0, getSentence().length - 1); clearSentence(); writeToSentence(temp); + // if the user presses left shift or right shift do nothing } else if (keyStroke != 161 && keyStroke != 160) { var key = String.fromCharCode(keyStroke); key = !lowercase ? key.toLowerCase() : key; @@ -89,6 +100,7 @@ class WinLogger extends SentenceLogger { Pointer.fromFunction(_mouseCallback, 0), NULL, 0); } + // Gets the name of the app that is currently in focus String _getFGAppName() { int nChar = 256; Pointer sPtr = malloc.allocate(nChar); @@ -102,10 +114,12 @@ class WinLogger extends SentenceLogger { sPtr.toDartString().substring(0, sPtr.toDartString().length - 4)); } + // Formats the name of the app to be more readable String _formatName(String name) { return name[0].toUpperCase() + name.toLowerCase().substring(1); } + // Finds the icon of an app based on the window handle Uint8List? findAppIcon(int hWnd, {background = 0xffffff, hover = false}) { var icon = SendMessage(hWnd, WM_GETICON, 2, 0); // ICON_SMALL2 - User Made Apps diff --git a/lib/main.dart b/lib/main.dart index 04e7d15..287d33f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,19 +30,23 @@ import 'package:negate/ui/common_ui.dart'; Future main() async { const loggerUI = ThemedHourlyUI(); + // This is to allow access to service bindings before the UI is displayed + // for things such as shared preferences and device specific directories WidgetsFlutterBinding.ensureInitialized(); + // Use a secure storage method for the database final dbFolder = await getApplicationSupportDirectory(); final dbString = p.join(dbFolder.path, 'db.sqlite'); final rPort = ReceivePort(); var prefs = await SharedPreferences.getInstance(); + // Set translation to off by default int translate = 0; var analyser = SentimentAnalysis(); await analyser.init(); if (prefs.getBool('translate') == null) { + // Check if the system contains a language other than English final List systemLocales = WidgetsBinding.instance.window.locales; - log(systemLocales.toString()); if (systemLocales.length > 1 || systemLocales.where((locale) => !locale.languageCode.contains('en')).isNotEmpty) { prefs.setBool('translate', true); } @@ -56,6 +60,8 @@ Future main() async { } } } + // Pass analyser to logger isolate as initialisation is not possible within the isolate + // as only the main isolate can access the service bindings var tfp = TfParams(analyser.sInterpreter.address, analyser.dictionary, translate); if (prefs.getBool('dynamic_theme') == null) { @@ -64,25 +70,27 @@ Future main() async { } else { getIt.registerSingleton(prefs.getBool('dynamic_theme')!); } - prefs.getBool('average_sentiment') == null - ? prefs.setBool('average_sentiment', true) - : null; - prefs.getDouble('multiplier_sentiment') == null - ? prefs.setDouble('multiplier_sentiment', 0.75) - : null; prefs.getString('blacklist') == null ? prefs.setString('blacklist', LoggerFactory.getLoggerRegex().pattern) : null; if (Platform.isAndroid || Platform.isIOS) { + // Start the logger within the main isolate on mobile + // as service bindings are not available within isolates + // and the logger needs access to android's accessibility service LoggerFactory.startLoggerFactory( TfliteRequest(rPort.sendPort, dbString, tfp, prefs)); } else { + // Start the logger within an isolate on desktop + // as desktop hooks require their own constantly running thread + // otherwise the hooks will not be called await loggerUI.initSystemTray(); await Isolate.spawn(LoggerFactory.startLoggerFactory, TfliteRequest(rPort.sendPort, dbString, tfp, prefs)); } + // Receive the send port from the logger isolate for the drift database + // and connect to the database and register it for the UI to use var iPort = await rPort.first as SendPort; var isolate = DriftIsolate.fromConnectPort(iPort); var sdb = SentimentDB.connect(await isolate.connect()); @@ -131,6 +139,8 @@ class ThemedHourlyUI extends StatelessWidget { @override Widget build(BuildContext context) { Widget home = HourlyDashboard(); + // Prevent building the window decorations on unit tests as they are not + // supported on the test platform if ((Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !Platform.environment.containsKey('FLUTTER_TEST')) { home = Column( @@ -140,6 +150,7 @@ class ThemedHourlyUI extends StatelessWidget { ], ); } else { + // On mobile, use a foreground task to keep the app running home = WithForegroundTask(child: home); } @@ -198,6 +209,8 @@ class HourlyDashboard extends ConsumerWidget { } else { if (Platform.isAndroid && !_requested) { _requested = true; + // Start the accessibility service on android only after the user has + // accepted the privacy policy (Google Play Store requires this) AndroidLogger().startAccessibility(); } } @@ -344,6 +357,7 @@ class HourlyDashboard extends ConsumerWidget { ref.read(dbProvider.notifier).set(slog); }, onError: (err, stk) => log(err)); }, + // Manual refresh required, as the database is updated in the background child: const Text('Update logs'), ), ], @@ -353,6 +367,7 @@ class HourlyDashboard extends ConsumerWidget { Widget bottomTitles(double value, TitleMeta meta) { const style = TextStyle(fontWeight: FontWeight.normal, fontSize: 14); String text; + // Only show every 6th hour for readability if (value == 0) { text = '12 AM'; } else if (value == 6) { diff --git a/lib/sentiment_analysis.dart b/lib/sentiment_analysis.dart index 18fe22b..7fe0bb0 100644 --- a/lib/sentiment_analysis.dart +++ b/lib/sentiment_analysis.dart @@ -58,6 +58,9 @@ class SentimentAnalysis { } Future classify(String rawText) async { + // If translate is enabled, translate the text to english + // using google ml kit on device translation, + // source language is detected automatically if (_translate == 2) { log(rawText); final String response = diff --git a/lib/sentiment_db.dart b/lib/sentiment_db.dart index 29a74c6..2e2ca13 100644 --- a/lib/sentiment_db.dart +++ b/lib/sentiment_db.dart @@ -16,17 +16,24 @@ import 'package:shared_preferences/shared_preferences.dart'; part 'sentiment_db.g.dart'; class SentimentLogs extends Table { + // Name of the app being logged TextColumn get name => text().withLength(min: 3, max: 256)(); + // Hour of the day the app was last logged DateTimeColumn get hour => dateTime()(); + // Time used in the app in seconds IntColumn get timeUsed => integer()(); + // Average sentiment score of the app RealColumn get avgScore => real()(); @override + // we want to make sure that each app has only one entry per hour Set get primaryKey => {name, hour}; } class AppIcons extends Table { + // Name of the app being logged TextColumn get name => text().withLength(min: 3, max: 256)(); + // Icon of the app BlobColumn get icon => blob()(); @override @@ -37,7 +44,6 @@ class AppIcons extends Table { class SentimentDB extends _$SentimentDB { // we tell the database where to store the data with this constructor SentimentDB() : super(_openConnection()); - //SentimentDB.ndb(NativeDatabase db): super(LazyDatabase(() async {return db;})); SentimentDB.connect(DatabaseConnection connection) : super.connect(connection); @@ -52,6 +58,9 @@ class SentimentDB extends _$SentimentDB { return ico.icon; } + // Returns a list of names of all the apps that have had their icon logged, + // this allows the logger to check if the app has been logged before and + // not log it again without having to have the icon of every app in memory Future> getListOfIcons() async { var set = HashSet(); var iconList = @@ -62,6 +71,8 @@ class SentimentDB extends _$SentimentDB { return set; } + // JSON encoder, required for the date time object as JSON does not have a + // date time object standard dynamic encoder(dynamic item) { Map encodedItem = {}; if (item is SentimentLog) { @@ -73,6 +84,7 @@ class SentimentDB extends _$SentimentDB { return encodedItem; } + // JSON decoder dynamic reviver(dynamic key, dynamic value) { if (key == 'hour') { return DateTime.parse(value); @@ -107,19 +119,26 @@ class SentimentDB extends _$SentimentDB { } } + // gets the average sentiment score of all the apps used within a specified + // period of time Future>>> _getSentimentsByName( DateTime after, [DateTime? before]) async { + // if before is null, then we only use after for the range of dates to fetch var query = select(sentimentLogs) ..where((tbl) => tbl.hour.isBiggerOrEqualValue(after)); if (before != null) { query = select(sentimentLogs) ..where((tbl) => tbl.hour.isBetweenValues(after, before)); } + // Create empty maps to store the average sentiment score and time used Map> weeklyAverage = >{}; Map weeklyCount = {}; var res = await query.get(); for (var log in res) { + // if the app has already been logged, then we add the new sentiment score + // to the overall average and time used to the existing values + // otherwise we add the app to the map if (weeklyAverage.containsKey(log.name)) { weeklyAverage[log.name]![0] = ((weeklyAverage[log.name]![0] * weeklyCount[log.name]!) + @@ -137,14 +156,19 @@ class SentimentDB extends _$SentimentDB { return weeklyAverage.entries.toList(); } + // Generates a list of the top 5 most positive and negative apps used within + // a specified period of time and returns them as a list of lists Future>>>> getRecommendations( DateTime after) async { var sorted = await _getSentimentsByName(after); //Ignore apps used for less than 10 minutes sorted.removeWhere((element) => element.value[1] < 10); + //Sort the list in ascending order by the average sentiment score sorted.sort((a, b) => a.value[0].compareTo(b.value[0])); var negative = sorted; + // reverse the list to get the most positive apps at the top var positive = sorted.reversed.toList(); + // only show the top 5 apps for each list if (sorted.length > 5) { negative = sorted.sublist(0, 5); positive = sorted.reversed.toList().sublist(0, 5); @@ -152,11 +176,15 @@ class SentimentDB extends _$SentimentDB { return [negative, positive]; } + // Gets the data for the daily breakdown pie chart Future>>> getDailyBreakdown( DateTime date) async { + // align the date to the start of the day var selectedDate = date.alignDateTime(const Duration(days: 1)); + // get the sentiment logs for the selected date var sentiments = await _getSentimentsByName( selectedDate, selectedDate.add(const Duration(days: 1))); + // sort the list in descending order by the time used sentiments.sort((b, a) => a.value[1].compareTo(b.value[1])); var sub = sentiments; double totalTime = 0; @@ -165,11 +193,16 @@ class SentimentDB extends _$SentimentDB { for (var sentiment in sentiments) { totalTime += sentiment.value[1]; + // if more than 7 apps have been used, then we only show the top 7 + // and combine the rest into an 'Other' category. + // for this reason we need to know the total time used by the top 7 apps + // for the 'Other' category to be displayed correctly if (counter == 7) { subTime = totalTime; } counter++; } + // calculate the percentage of time used for each app for (var sentiment in sub) { sentiment.value[2] = sentiment.value[1] / totalTime; } @@ -181,6 +214,7 @@ class SentimentDB extends _$SentimentDB { return sub; } + // Gets the data for the hourly breakdown line chart Future> getAvgHourlySentiment(DateTime date) async { var query = (select(sentimentLogs) ..where((tbl) => @@ -190,6 +224,7 @@ class SentimentDB extends _$SentimentDB { var res = await query.get(); var averages = List.filled(24, 0); var totalTime = List.filled(24, 0); + // builds a list of the average sentiment score for each hour of the day for (var i in res) { averages[i.hour.hour] += i.avgScore * i.timeUsed; totalTime[i.hour.hour] += i.timeUsed; @@ -203,6 +238,7 @@ class SentimentDB extends _$SentimentDB { return averages; } + // Gets the sentiment logs for a specified hour of the day, sorted by time used Future> getDaySentiment(DateTime time) async { return await (select(sentimentLogs) ..where((tbl) { @@ -223,6 +259,7 @@ class SentimentDB extends _$SentimentDB { sdb.into(sdb.appIcons).insert(entry); } + // Adds a list of sentiment logs to the database static Future addSentiments(AddSentimentRequest r) async { var isolate = DriftIsolate.fromConnectPort(r.iPort); var sdb = SentimentDB.connect(await isolate.connect()); @@ -231,6 +268,7 @@ class SentimentDB extends _$SentimentDB { for (var log in r.sentiments.entries) { var entry = SentimentLogsCompanion( name: Value(log.key), + // align the date to the start of the hour hour: Value( log.value.lastTimeUsed.alignDateTime(const Duration(hours: 1))), timeUsed: Value(log.value.totalTimeUsed.ceil()), @@ -243,6 +281,7 @@ class SentimentDB extends _$SentimentDB { } } +// Extension to align a DateTime to a specified Duration extension Alignment on DateTime { DateTime alignDateTime(Duration alignment, [bool roundUp = false]) { assert(alignment >= Duration.zero); @@ -288,6 +327,8 @@ LazyDatabase _openConnection() { }); } +// Required as Isolates can only pass a single object and the database +// has to run on a separate isolate for cross isolate communication class IsolateStartRequest { final SendPort sendDriftIsolate; final String targetPath; diff --git a/lib/ui/common_ui.dart b/lib/ui/common_ui.dart index 1a376e1..8219803 100644 --- a/lib/ui/common_ui.dart +++ b/lib/ui/common_ui.dart @@ -8,14 +8,14 @@ import 'package:intl/intl.dart'; import 'package:negate/logger/logger_factory.dart'; import 'package:negate/sentiment_db.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:negate/logger/android_logger.dart'; import 'globals.dart'; class CommonUI { - static bool firstPage = true; + // check if the user has moved to he next page + static bool _firstPage = true; static Widget infoPage(BuildContext context) { return Scaffold( @@ -59,7 +59,8 @@ class CommonUI { static Future showDisclosure( BuildContext context, SharedPreferences pref) async { var firstPage = LoggerFactory.getDisclosureText(context); - var secondPage = SingleChildScrollView(child: ListBody( + var secondPage = SingleChildScrollView( + child: ListBody( children: const [ Text("Privacy Policy", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), @@ -89,7 +90,7 @@ class CommonUI { return AlertDialog( scrollable: true, title: const Text('Privacy Disclosure'), - content: CommonUI.firstPage ? firstPage : secondPage, + content: CommonUI._firstPage ? firstPage : secondPage, actions: [ TextButton( style: TextButton.styleFrom( @@ -107,13 +108,13 @@ class CommonUI { style: TextButton.styleFrom( textStyle: const TextStyle(fontSize: 20), ), - child: CommonUI.firstPage + child: CommonUI._firstPage ? const Text('Next') : const Text('Accept'), onPressed: () { - if (CommonUI.firstPage) { + if (CommonUI._firstPage) { setState(() { - CommonUI.firstPage = false; + CommonUI._firstPage = false; }); return; } @@ -178,6 +179,7 @@ class CommonUI { ); } + // colours the bar based on the positivity of the value static Color getBarColour(double val) { int percent = (val * 100).round(); if (percent >= 75) { @@ -195,6 +197,7 @@ class CommonUI { } } + // creates a listview of the app sentiment logs passed in including the app icon static Widget appListView( List>> logs, SentimentDB sdb) { return ListView.separated( diff --git a/lib/ui/globals.dart b/lib/ui/globals.dart index f259931..397d6f2 100644 --- a/lib/ui/globals.dart +++ b/lib/ui/globals.dart @@ -3,13 +3,19 @@ import 'package:get_it/get_it.dart'; import '../sentiment_db.dart'; +// this is the provider that will be used to access the database +// and update the UI every time it changes final dbProvider = StateNotifierProvider((ref) { return DBMonitor(); }); +// this holds the database instance and any other globals for the UI final getIt = GetIt.instance; +// the date of the currently selected day in the UI DateTime selectedDate = DateTime.now(); +// underlying class for the provider which holds the logs from the database +// for the UI to display class DBMonitor extends StateNotifier> { DBMonitor() : super([]); diff --git a/lib/ui/settings.dart b/lib/ui/settings.dart index 56da4d7..ada7ad5 100644 --- a/lib/ui/settings.dart +++ b/lib/ui/settings.dart @@ -24,6 +24,7 @@ class SettingsPage { body: FutureBuilder( future: SharedPreferences.getInstance(), builder: (context, prefs) { + // the future can be null in the first few calls if (prefs.data == null) { return const Text('Loading...'); } diff --git a/pubspec.lock b/pubspec.lock index d7e921c..050db4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + sha256: e440ac42679dfc04bbbefb58ed225c994bc7e07fccc8a68ec7d3631a127e5da9 url: "https://pub.dev" source: hosted - version: "52.0.0" + version: "54.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + sha256: "2c2e3721ee9fb36de92faa060f3480c81b23e904352b087e5c64224b1a044427" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.6.0" analyzer_plugin: dependency: transitive description: @@ -117,18 +117,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" build_runner: dependency: "direct dev" description: @@ -349,10 +349,10 @@ packages: dependency: "direct main" description: name: fl_chart - sha256: "155556c4aba56c8474308d51b4e5446b80a07db9ab66a3cb74aff05c110df982" + sha256: e97c5b850ad056e9b3a85d3afeb44c239a83aa994a90723940dac82234f2efaf url: "https://pub.dev" source: hosted - version: "0.60.0" + version: "0.61.0" flutter: dependency: "direct main" description: flutter @@ -370,18 +370,18 @@ packages: dependency: "direct main" description: name: flutter_foreground_task - sha256: "5eb81adfd98c77f4d4803ae80d7e0573fd35f706fce6aaa7376656a15ba2d1e0" + sha256: a7bafaec32e33ec1670906850241340fd721ae040ed23db03cb7216848afbbbc url: "https://pub.dev" source: hosted - version: "3.10.0" + version: "4.1.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + sha256: "02dcaf49d405f652b7160e882bacfc02cb497041bb2eab2a49b1c393cf9aac12" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.12.0" flutter_lints: dependency: "direct dev" description: @@ -402,18 +402,18 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "46a27b7a11dc13738054093076f2dc65692ddcd463979b15092accf5681aea20" + sha256: "40c0d7d03ccd24fa32ea08dcfc4df9bb92c4c26c9a91938945da3be10ed8ca83" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -500,10 +500,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.15" intl: dependency: "direct main" description: @@ -556,10 +556,10 @@ packages: dependency: transitive description: name: markdown - sha256: "4ed544d2ce84975b2ab5cbd4268f2d31f47858553ae2295c92fdf5d6e431a927" + sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" matcher: dependency: transitive description: @@ -596,10 +596,10 @@ packages: dependency: "direct dev" description: name: msix - sha256: e3de4d9f52543ad6e4b0f534991e1303cbd379d24be28dd241ac60bd9439a201 + sha256: "765e6b4d9f571956014d85062614468993d9e4b998bb57f2b727845b6d99c6f1" url: "https://pub.dev" source: hosted - version: "3.7.0" + version: "3.8.2" package_config: dependency: transitive description: @@ -620,50 +620,50 @@ packages: dependency: "direct main" description: name: path_provider - sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e" url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.23" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9" + sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a" url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.1.9" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" permission_handler: dependency: "direct main" description: @@ -724,10 +724,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pointycastle: dependency: transitive description: @@ -764,10 +764,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" quiver: dependency: transitive description: @@ -788,10 +788,10 @@ packages: dependency: transitive description: name: riverpod - sha256: "59a48de9c757aa61aa28e9fd625ffb360d43b6b54606f12536622c55be9e8c4b" + sha256: c5aea6c3fed340707f013a023a37ab388bf45611a8a4f7e76b5e9007eb76cd25 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" settings_ui: dependency: "direct main" description: @@ -804,66 +804,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9" + sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.18" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7" + sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4 url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - shared_preferences_ios: - dependency: transitive - description: - name: shared_preferences_ios - sha256: "585a14cefec7da8c9c2fb8cd283a3bb726b4155c0952afe6a0caaa7b2272de34" + sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874 + sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3 + sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958 + sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.5" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9" + sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" shelf: dependency: transitive description: @@ -913,10 +905,10 @@ packages: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: ebfdab73610bbd2ec01d8f367b7a1b49ad3a01f398df436f283e4063173ceb7b + sha256: "02f80aea54a19a36b347dedf6d4181ecd9107f5831ea6139cfd0376a3de197ba" url: "https://pub.dev" source: hosted - version: "0.5.12" + version: "0.5.13" sqlparser: dependency: transitive description: @@ -1018,66 +1010,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" url: "https://pub.dev" source: hosted - version: "6.1.9" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732" url: "https://pub.dev" source: hosted - version: "6.0.23" + version: "6.0.24" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815" + sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.15" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b9beb8e..e714983 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.2+16 +version: 1.2.0+20 environment: sdk: '>=2.18.2 <3.0.0' @@ -49,10 +49,10 @@ dependencies: git: url: https://github.com/Miko2x/tflite_flutter_plugin ref: master - flutter_foreground_task: ^3.10.0 + flutter_foreground_task: ^4.1.0 flutter_accessibility_service: ^0.2.2 device_apps: ^2.2.0 - fl_chart: ^0.60.0 + fl_chart: ^0.61.0 system_tray: ^2.0.2 bitsdojo_window: ^0.1.5 permission_handler: ^10.2.0 @@ -69,7 +69,7 @@ dependencies: dev_dependencies: drift_dev: ^2.4.0 - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.12.0 build_runner: ^2.3.3 flutter_test: sdk: flutter