diff --git a/lib/custom_errors.dart b/lib/custom_errors.dart index a1a9902..14b785f 100644 --- a/lib/custom_errors.dart +++ b/lib/custom_errors.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:obtainium/providers/logs_provider.dart'; +import 'package:provider/provider.dart'; class ObtainiumError { late String message; @@ -75,6 +77,8 @@ class MultiAppMultiError extends ObtainiumError { } showError(dynamic e, BuildContext context) { + Provider.of(context, listen: false) + .add(e.toString(), level: LogLevels.error); if (e is String || (e is ObtainiumError && !e.unexpected)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), diff --git a/lib/main.dart b/lib/main.dart index 8b2c51f..eb79041 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:obtainium/custom_errors.dart'; import 'package:obtainium/pages/home.dart'; import 'package:obtainium/providers/apps_provider.dart'; +import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; @@ -15,7 +16,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:android_alarm_manager_plus/android_alarm_manager_plus.dart'; -const String currentVersion = '0.7.1'; +const String currentVersion = '0.7.2'; const String currentReleaseTag = 'v$currentVersion-beta'; // KEEP THIS IN SYNC WITH GITHUB RELEASES @@ -23,12 +24,15 @@ const int bgUpdateCheckAlarmId = 666; @pragma('vm:entry-point') Future bgUpdateCheck(int taskId, Map? params) async { + LogsProvider logs = LogsProvider(); + logs.add('Started BG update check task'); int? ignoreAfterMicroseconds = params?['ignoreAfterMicroseconds']; WidgetsFlutterBinding.ensureInitialized(); await AndroidAlarmManager.initialize(); DateTime? ignoreAfter = ignoreAfterMicroseconds != null ? DateTime.fromMicrosecondsSinceEpoch(ignoreAfterMicroseconds) : null; + logs.add('Bg update ignoreAfter is $ignoreAfter'); var notificationsProvider = NotificationsProvider(); await notificationsProvider.notify(checkingUpdatesNotification); try { @@ -40,17 +44,18 @@ Future bgUpdateCheck(int taskId, Map? params) async { DateTime nextIgnoreAfter = DateTime.now(); String? err; try { + logs.add('Started actual BG update checking'); await appsProvider.checkUpdates( ignoreAppsCheckedAfter: ignoreAfter, throwErrorsForRetry: true); } catch (e) { if (e is RateLimitError || e is SocketException) { - AndroidAlarmManager.oneShot( - Duration(minutes: e is RateLimitError ? e.remainingMinutes : 15), - Random().nextInt(pow(2, 31) as int), - bgUpdateCheck, - params: { - 'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch - }); + var remainingMinutes = e is RateLimitError ? e.remainingMinutes : 15; + logs.add( + 'BG update checking encountered a ${e.runtimeType}, will schedule a retry check in $remainingMinutes minutes'); + AndroidAlarmManager.oneShot(Duration(minutes: remainingMinutes), + Random().nextInt(pow(2, 31) as int), bgUpdateCheck, params: { + 'ignoreAfterMicroseconds': nextIgnoreAfter.microsecondsSinceEpoch + }); } else { err = e.toString(); } @@ -74,7 +79,8 @@ Future bgUpdateCheck(int taskId, Map? params) async { // silentlyUpdated.map((e) => appsProvider.apps[e]!.app).toList()), // cancelExisting: true); // } - + logs.add( + 'BG update checking found ${newUpdates.length} updates - will notify user if needed'); if (newUpdates.isNotEmpty) { notificationsProvider.notify(UpdateNotification(newUpdates)); } @@ -85,6 +91,7 @@ Future bgUpdateCheck(int taskId, Map? params) async { notificationsProvider .notify(ErrorCheckingUpdatesNotification(e.toString())); } finally { + logs.add('Finished BG update check task'); await notificationsProvider.cancel(checkingUpdatesNotification.id); } } @@ -102,7 +109,8 @@ void main() async { providers: [ ChangeNotifierProvider(create: (context) => AppsProvider()), ChangeNotifierProvider(create: (context) => SettingsProvider()), - Provider(create: (context) => NotificationsProvider()) + Provider(create: (context) => NotificationsProvider()), + Provider(create: (context) => LogsProvider()) ], child: const Obtainium(), )); @@ -124,12 +132,14 @@ class _ObtainiumState extends State { Widget build(BuildContext context) { SettingsProvider settingsProvider = context.watch(); AppsProvider appsProvider = context.read(); + LogsProvider logs = context.read(); if (settingsProvider.prefs == null) { settingsProvider.initializeSettings(); } else { bool isFirstRun = settingsProvider.checkAndFlipFirstRun(); if (isFirstRun) { + logs.add('This is the first ever run of Obtainium'); // If this is the first run, ask for notification permissions and add Obtainium to the Apps list Permission.notification.request(); appsProvider.saveApps([ @@ -149,6 +159,10 @@ class _ObtainiumState extends State { } // Register the background update task according to the user's setting if (existingUpdateInterval != settingsProvider.updateInterval) { + if (existingUpdateInterval != -1) { + logs.add( + 'Setting update interval to ${settingsProvider.updateInterval}'); + } existingUpdateInterval = settingsProvider.updateInterval; if (existingUpdateInterval == 0) { AndroidAlarmManager.cancel(bgUpdateCheckAlarmId); diff --git a/lib/pages/settings.dart b/lib/pages/settings.dart index 85521e6..4498a91 100644 --- a/lib/pages/settings.dart +++ b/lib/pages/settings.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:obtainium/components/custom_app_bar.dart'; import 'package:obtainium/components/generated_form.dart'; +import 'package:obtainium/components/generated_form_modal.dart'; +import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:obtainium/providers/source_provider.dart'; import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SettingsPage extends StatefulWidget { @@ -238,23 +242,55 @@ class _SettingsPageState extends State { SliverToBoxAdapter( child: Column( children: [ - height16, - TextButton.icon( - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return Colors.grey; - }), - ), - onPressed: () { - launchUrlString(settingsProvider.sourceUrl, - mode: LaunchMode.externalApplication); - }, - icon: const Icon(Icons.code), - label: Text( - 'Source', - style: Theme.of(context).textTheme.bodySmall, - ), + const Divider( + height: 32, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton.icon( + onPressed: () { + launchUrlString(settingsProvider.sourceUrl, + mode: LaunchMode.externalApplication); + }, + icon: const Icon(Icons.code), + label: const Text( + 'App Source', + ), + ), + TextButton.icon( + onPressed: () { + context.read().get().then((logs) { + if (logs.isEmpty) { + showError(ObtainiumError('No Logs'), context); + } else { + String logString = + logs.map((e) => e.toString()).join('\n\n'); + showDialog( + context: context, + builder: (BuildContext ctx) { + return GeneratedFormModal( + title: 'Obtainium App Logs', + items: const [], + defaultValues: const [], + message: logString, + initValid: true, + ); + }).then((value) { + if (value != null) { + Share.share( + logs + .map((e) => e.toString()) + .join('\n\n'), + subject: 'Obtainium App Logs'); + } + }); + } + }); + }, + icon: const Icon(Icons.bug_report_outlined), + label: const Text('App Logs')), + ], ), height16, ], diff --git a/lib/providers/apps_provider.dart b/lib/providers/apps_provider.dart index fea48e6..382a04f 100644 --- a/lib/providers/apps_provider.dart +++ b/lib/providers/apps_provider.dart @@ -12,8 +12,8 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:install_plugin_v2/install_plugin_v2.dart'; import 'package:installed_apps/app_info.dart'; import 'package:installed_apps/installed_apps.dart'; -import 'package:obtainium/app_sources/github.dart'; import 'package:obtainium/custom_errors.dart'; +import 'package:obtainium/providers/logs_provider.dart'; import 'package:obtainium/providers/notifications_provider.dart'; import 'package:obtainium/providers/settings_provider.dart'; import 'package:package_archive_info/package_archive_info.dart'; @@ -43,6 +43,7 @@ class AppsProvider with ChangeNotifier { bool loadingApps = false; bool gettingUpdates = false; bool forBGTask = false; + LogsProvider logs = LogsProvider(); // Variables to keep track of the app foreground status (installs can't run in the background) bool isForeground = true; diff --git a/lib/providers/logs_provider.dart b/lib/providers/logs_provider.dart new file mode 100644 index 0000000..83a634e --- /dev/null +++ b/lib/providers/logs_provider.dart @@ -0,0 +1,109 @@ +import 'package:flutter/foundation.dart'; +import 'package:sqflite/sqflite.dart'; + +const String logTable = 'logs'; +const String idColumn = '_id'; +const String levelColumn = 'level'; +const String messageColumn = 'message'; +const String timestampColumn = 'timestamp'; +const String dbPath = 'logs.db'; + +enum LogLevels { debug, info, warning, error } + +class Log { + int? id; + late LogLevels level; + late String message; + DateTime timestamp = DateTime.now(); + + Map toMap() { + var map = { + idColumn: id, + levelColumn: level.index, + messageColumn: message, + timestampColumn: timestamp.millisecondsSinceEpoch + }; + return map; + } + + Log(this.message, this.level); + + Log.fromMap(Map map) { + id = map[idColumn] as int; + level = LogLevels.values.elementAt(map[levelColumn] as int); + message = map[messageColumn] as String; + timestamp = + DateTime.fromMillisecondsSinceEpoch(map[timestampColumn] as int); + } + + @override + String toString() { + return '${timestamp.toString()}: ${level.name}: $message'; + } +} + +class LogsProvider { + LogsProvider({bool runDefaultClear = true}) { + clear(before: DateTime.now().subtract(const Duration(days: 7))); + } + + Database? db; + + Future getDB() async { + db ??= await openDatabase(dbPath, version: 1, + onCreate: (Database db, int version) async { + await db.execute(''' +create table if not exists $logTable ( + $idColumn integer primary key autoincrement, + $levelColumn integer not null, + $messageColumn text not null, + $timestampColumn integer not null) +'''); + }); + return db!; + } + + Future add(String message, {LogLevels level = LogLevels.info}) async { + Log l = Log(message, level); + l.id = await (await getDB()).insert(logTable, l.toMap()); + if (kDebugMode) { + print(l); + } + return l; + } + + Future> get({DateTime? before, DateTime? after}) async { + var where = getWhereDates(before: before, after: after); + return (await (await getDB()) + .query(logTable, where: where.key, whereArgs: where.value)) + .map((e) => Log.fromMap(e)) + .toList(); + } + + Future clear({DateTime? before, DateTime? after}) async { + var where = getWhereDates(before: before, after: after); + var res = await (await getDB()) + .delete(logTable, where: where.key, whereArgs: where.value); + if (res > 0) { + add('Cleared $res logs (before = $before, after = $after)'); + } + return res; + } +} + +MapEntry?> getWhereDates( + {DateTime? before, DateTime? after}) { + List where = []; + List whereArgs = []; + if (before != null) { + where.add('$timestampColumn < ?'); + whereArgs.add(before.millisecondsSinceEpoch); + } + if (after != null) { + where.add('$timestampColumn > ?'); + whereArgs.add(after.millisecondsSinceEpoch); + } + return whereArgs.isEmpty + ? const MapEntry(null, null) + : MapEntry(where.join(' and '), whereArgs); +} diff --git a/pubspec.lock b/pubspec.lock index a20cd86..665787d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -567,6 +567,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.9.0" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+3" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0+2" stack_trace: dependency: transitive description: @@ -588,6 +602,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0+3" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ed1000..0e2fc71 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: 0.7.1+57 # When changing this, update the tag in main() accordingly +version: 0.7.2+58 # When changing this, update the tag in main() accordingly environment: sdk: '>=2.18.2 <3.0.0' @@ -56,6 +56,7 @@ dependencies: installed_apps: ^1.3.1 package_archive_info: ^0.1.0 android_alarm_manager_plus: ^2.1.0 + sqflite: ^2.2.0+3 dev_dependencies: