From 620c840ccceee0e0c3904a32cd1c57d9fbc39868 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Fri, 10 Sep 2021 19:46:15 +0800 Subject: [PATCH] Capture logs from settings for bug report --- .../nc_photos/NotificationChannelHandler.kt | 45 +++++++++++++ android/app/src/main/res/values/strings.xml | 2 + lib/bloc/list_person.dart | 15 ----- lib/debug_util.dart | 42 ++++++++++++ lib/l10n/app_en.arb | 13 ++++ lib/l10n/untranslated-messages.txt | 19 ++++++ lib/main.dart | 2 + lib/mobile/android/notification.dart | 5 ++ lib/mobile/notification.dart | 12 ++++ lib/platform/notification.dart | 4 ++ lib/widget/settings.dart | 66 +++++++++++++++++++ 11 files changed, 210 insertions(+), 15 deletions(-) diff --git a/android/app/src/main/kotlin/com/nkming/nc_photos/NotificationChannelHandler.kt b/android/app/src/main/kotlin/com/nkming/nc_photos/NotificationChannelHandler.kt index 8ebb57e6..bcf7c926 100644 --- a/android/app/src/main/kotlin/com/nkming/nc_photos/NotificationChannelHandler.kt +++ b/android/app/src/main/kotlin/com/nkming/nc_photos/NotificationChannelHandler.kt @@ -48,6 +48,13 @@ class NotificationChannelHandler(activity: Activity) } catch (e: Throwable) { result.error("systemException", e.toString(), null) } + } else if (call.method == "notifyLogSaveSuccessful") { + try { + notifyLogSaveSuccessful(call.argument("fileUri")!!, + result) + } catch (e: Throwable) { + result.error("systemException", e.toString(), null) + } } else { result.notImplemented() } @@ -130,6 +137,44 @@ class NotificationChannelHandler(activity: Activity) result.success(null) } + private fun notifyLogSaveSuccessful(fileUri: String, + result: MethodChannel.Result) { + val uri = Uri.parse(fileUri) + val mimeType = "text/plain" + val builder = NotificationCompat.Builder(_context, DOWNLOAD_CHANNEL_ID) + .setSmallIcon(R.drawable.baseline_download_white_18) + .setWhen(System.currentTimeMillis()) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSound(RingtoneManager.getDefaultUri( + RingtoneManager.TYPE_NOTIFICATION)) + .setAutoCancel(true) + .setLocalOnly(true) + .setTicker(_context.getString( + R.string.log_save_successful_notification_title)) + .setContentTitle(_context.getString( + R.string.log_save_successful_notification_title)) + .setContentText(_context.getString( + R.string.log_save_successful_notification_text)) + + val openIntent = Intent().apply { + action = Intent.ACTION_VIEW + setDataAndType(uri, mimeType) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + val openPendingIntent = PendingIntent.getActivity(_context, 0, + openIntent, PendingIntent.FLAG_UPDATE_CURRENT) + builder.setContentIntent(openPendingIntent) + + // can't add the share action here because android will share the URI as + // plain text instead of treating it as a text file... + + with(NotificationManagerCompat.from(_context)) { + notify(DOWNLOAD_NOTIFICATION_ID, builder.build()) + } + result.success(null) + } + private fun loadNotificationImage(fileUri: Uri): Bitmap? { try { val resolver = _context.applicationContext.contentResolver diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index fb33b517..782d059a 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -9,4 +9,6 @@ SHARE Share with: Downloaded %1$s items successfully + Logs saved successfully + Tap to view your saved logs diff --git a/lib/bloc/list_person.dart b/lib/bloc/list_person.dart index 73605337..db98e527 100644 --- a/lib/bloc/list_person.dart +++ b/lib/bloc/list_person.dart @@ -71,21 +71,6 @@ class ListPersonBlocFailure extends ListPersonBlocState { class ListPersonBloc extends Bloc { ListPersonBloc() : super(ListPersonBlocInit()); - static ListPersonBloc of(Account account) { - final id = "${account.scheme}://${account.username}@${account.address}"; - try { - _log.fine("[of] Resolving bloc for '$id'"); - return KiwiContainer().resolve("ListPersonBloc($id)"); - } catch (_) { - // no created instance for this account, make a new one - _log.info("[of] New bloc instance for account: $account"); - final bloc = ListPersonBloc(); - KiwiContainer() - .registerInstance(bloc, name: "ListPersonBloc($id)"); - return bloc; - } - } - @override mapEventToState(ListPersonBlocEvent event) async* { _log.info("[mapEventToState] $event"); diff --git a/lib/debug_util.dart b/lib/debug_util.dart index b780ecca..53315c21 100644 --- a/lib/debug_util.dart +++ b/lib/debug_util.dart @@ -1,3 +1,45 @@ +import 'dart:convert'; + import 'package:flutter/foundation.dart'; +import 'package:nc_photos/mobile/platform.dart' + if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; + +class LogCapturer { + factory LogCapturer() { + if (_inst == null) { + _inst = LogCapturer._(); + } + return _inst!; + } + + LogCapturer._(); + + /// Start capturing logs + void start() { + _isEnable = true; + } + + /// Stop capturing and save the captured logs + Future stop() { + _isEnable = false; + final saver = platform.FileSaver(); + final content = Utf8Encoder().convert(_logs.join("\n")); + _logs.clear(); + return saver.saveFile("nc-photos.log", content); + } + + void onLog(String log) { + if (_isEnable) { + _logs.add(log); + } + } + + bool get isEnable => _isEnable; + + final _logs = []; + bool _isEnable = false; + + static LogCapturer? _inst; +} const bool shouldLogFileName = kDebugMode; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5d47ac85..40dd5ead 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -361,6 +361,11 @@ "@settingsBugReportTitle": { "description": "Report issue" }, + "settingsCaptureLogsTitle": "Capture logs", + "@settingsCaptureLogsTitle": { + "description": "Capture app logs for bug report" + }, + "settingsCaptureLogsDescription": "Help developer to diagnose bugs", "settingsTranslatorTitle": "Translator", "@settingsTranslatorTitle": { "description": "Title of the translator item" @@ -381,6 +386,14 @@ "@exifSupportConfirmationDialogTitle": { "description": "Title of the dialog to confirm enabling exif support" }, + "captureLogDetails": "To take logs for a bug report:\n\n1. Enable this setting\n2. Reproduce the issue\n3. Disable this setting\n4. Look for nc-photos.log in the download folder\n\n*If the issue causes the app to crash, no logs could be captured. In such case, please contact the developer for further instructions", + "@captureLogDetails": { + "description": "Detailed description on capturing logs" + }, + "captureLogSuccessNotification": "Logs saved successfully", + "@captureLogSuccessNotification": { + "description": "Captured logs are successfully saved to the download directory" + }, "doneButtonLabel": "DONE", "@doneButtonLabel": { "description": "Label of the done button" diff --git a/lib/l10n/untranslated-messages.txt b/lib/l10n/untranslated-messages.txt index d1c92cf2..d2b9bd1a 100644 --- a/lib/l10n/untranslated-messages.txt +++ b/lib/l10n/untranslated-messages.txt @@ -15,6 +15,10 @@ "settingsUseBlackInDarkThemeTitle", "settingsUseBlackInDarkThemeTrueDescription", "settingsUseBlackInDarkThemeFalseDescription", + "settingsCaptureLogsTitle", + "settingsCaptureLogsDescription", + "captureLogDetails", + "captureLogSuccessNotification", "sortOptionAlbumNameLabel", "sortOptionAlbumNameDescendingLabel", "listEmptyText", @@ -51,6 +55,13 @@ "unmuteTooltip" ], + "es": [ + "settingsCaptureLogsTitle", + "settingsCaptureLogsDescription", + "captureLogDetails", + "captureLogSuccessNotification" + ], + "fr": [ "collectionsTooltip", "settingsViewerTitle", @@ -67,6 +78,10 @@ "settingsUseBlackInDarkThemeTitle", "settingsUseBlackInDarkThemeTrueDescription", "settingsUseBlackInDarkThemeFalseDescription", + "settingsCaptureLogsTitle", + "settingsCaptureLogsDescription", + "captureLogDetails", + "captureLogSuccessNotification", "sortOptionAlbumNameLabel", "sortOptionAlbumNameDescendingLabel", "helpTooltip", @@ -84,6 +99,10 @@ ], "ru": [ + "settingsCaptureLogsTitle", + "settingsCaptureLogsDescription", + "captureLogDetails", + "captureLogSuccessNotification", "sortOptionAlbumNameLabel", "sortOptionAlbumNameDescendingLabel" ] diff --git a/lib/main.dart b/lib/main.dart index 56d642f3..ab0d8641 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; +import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/mobile/self_signed_cert_manager.dart'; @@ -69,6 +70,7 @@ void _initLog() { msg = "\x1B[${color}m$msg\x1B[0m"; } debugPrint(msg); + LogCapturer().onLog(msg); }); } diff --git a/lib/mobile/android/notification.dart b/lib/mobile/android/notification.dart index d369a6be..ce5b7e39 100644 --- a/lib/mobile/android/notification.dart +++ b/lib/mobile/android/notification.dart @@ -8,6 +8,11 @@ class Notification { "mimeTypes": mimeTypes, }); + static Future notifyLogSaveSuccessful(String fileUri) => + _channel.invokeMethod("notifyLogSaveSuccessful", { + "fileUri": fileUri, + }); + static const _channel = const MethodChannel("com.nkming.nc_photos/notification"); } diff --git a/lib/mobile/notification.dart b/lib/mobile/notification.dart index 1cbfd93c..90e86ed4 100644 --- a/lib/mobile/notification.dart +++ b/lib/mobile/notification.dart @@ -13,3 +13,15 @@ class AndroidItemDownloadSuccessfulNotification final List fileUris; final List mimeTypes; } + +class AndroidLogSaveSuccessfulNotification + extends itf.LogSaveSuccessfulNotification { + AndroidLogSaveSuccessfulNotification(this.fileUri); + + @override + Future notify() { + return Notification.notifyLogSaveSuccessful(fileUri); + } + + final String fileUri; +} diff --git a/lib/platform/notification.dart b/lib/platform/notification.dart index 55a114c5..ef3b7573 100644 --- a/lib/platform/notification.dart +++ b/lib/platform/notification.dart @@ -1,3 +1,7 @@ abstract class ItemDownloadSuccessfulNotification { Future notify(); } + +abstract class LogSaveSuccessfulNotification { + Future notify(); +} diff --git a/lib/widget/settings.dart b/lib/widget/settings.dart index 5b6b0b48..fc11137d 100644 --- a/lib/widget/settings.dart +++ b/lib/widget/settings.dart @@ -4,11 +4,13 @@ import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/debug_util.dart'; import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/metadata_task_manager.dart'; import 'package:nc_photos/mobile/android/android_info.dart'; +import 'package:nc_photos/mobile/notification.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; @@ -122,6 +124,12 @@ class _SettingsState extends State { launch(_bugReportUrl); }, ), + SwitchListTile( + title: Text(L10n.global().settingsCaptureLogsTitle), + subtitle: Text(L10n.global().settingsCaptureLogsDescription), + value: LogCapturer().isEnable, + onChanged: (value) => _onCaptureLogChanged(context, value), + ), if (translator.isNotEmpty) ListTile( title: Text(L10n.global().settingsTranslatorTitle), @@ -251,6 +259,64 @@ class _SettingsState extends State { } } + void _onCaptureLogChanged(BuildContext context, bool value) async { + if (value) { + final result = await showDialog( + context: context, + builder: (context) => AppTheme( + child: AlertDialog( + content: Text(L10n.global().captureLogDetails), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(L10n.global().enableButtonLabel), + ), + ], + ), + ), + ); + if (result == true) { + setState(() { + LogCapturer().start(); + }); + } + } else { + if (LogCapturer().isEnable) { + setState(() { + LogCapturer().stop().then((result) { + _onLogSaveSuccessful(result); + }); + }); + } + } + } + + void _onLogSaveSuccessful(dynamic result) { + var notif; + if (platform_k.isAndroid) { + notif = AndroidLogSaveSuccessfulNotification(result); + } + if (notif != null) { + try { + notif.notify(); + return; + } catch (e, stacktrace) { + _log.shout( + "[_onLogSaveSuccessful] Failed showing platform notification", + e, + stacktrace); + } + } + + // fallback + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().downloadSuccessNotification), + duration: k.snackBarDurationShort, + )); + } + Future _setExifSupport(bool value) async { final oldValue = _isEnableExif; setState(() {