Capture logs from settings for bug report

This commit is contained in:
Ming Ming 2021-09-10 19:46:15 +08:00
parent 858455206c
commit 620c840ccc
11 changed files with 210 additions and 15 deletions

View file

@ -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<String>("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

View file

@ -9,4 +9,6 @@
<string name="download_successful_notification_action_share">SHARE</string>
<string name="download_successful_notification_action_share_chooser">Share with:</string>
<string name="download_multiple_successful_notification_title">Downloaded %1$s items successfully</string>
<string name="log_save_successful_notification_title">Logs saved successfully</string>
<string name="log_save_successful_notification_text">Tap to view your saved logs</string>
</resources>

View file

@ -71,21 +71,6 @@ class ListPersonBlocFailure extends ListPersonBlocState {
class ListPersonBloc extends Bloc<ListPersonBlocEvent, ListPersonBlocState> {
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>("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<ListPersonBloc>(bloc, name: "ListPersonBloc($id)");
return bloc;
}
}
@override
mapEventToState(ListPersonBlocEvent event) async* {
_log.info("[mapEventToState] $event");

View file

@ -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<dynamic> 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 = <String>[];
bool _isEnable = false;
static LogCapturer? _inst;
}
const bool shouldLogFileName = kDebugMode;

View file

@ -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"

View file

@ -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"
]

View file

@ -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);
});
}

View file

@ -8,6 +8,11 @@ class Notification {
"mimeTypes": mimeTypes,
});
static Future<void> notifyLogSaveSuccessful(String fileUri) =>
_channel.invokeMethod("notifyLogSaveSuccessful", <String, dynamic>{
"fileUri": fileUri,
});
static const _channel =
const MethodChannel("com.nkming.nc_photos/notification");
}

View file

@ -13,3 +13,15 @@ class AndroidItemDownloadSuccessfulNotification
final List<String> fileUris;
final List<String?> mimeTypes;
}
class AndroidLogSaveSuccessfulNotification
extends itf.LogSaveSuccessfulNotification {
AndroidLogSaveSuccessfulNotification(this.fileUri);
@override
Future<void> notify() {
return Notification.notifyLogSaveSuccessful(fileUri);
}
final String fileUri;
}

View file

@ -1,3 +1,7 @@
abstract class ItemDownloadSuccessfulNotification {
Future<void> notify();
}
abstract class LogSaveSuccessfulNotification {
Future<void> notify();
}

View file

@ -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<Settings> {
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<Settings> {
}
}
void _onCaptureLogChanged(BuildContext context, bool value) async {
if (value) {
final result = await showDialog<bool>(
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<void> _setExifSupport(bool value) async {
final oldValue = _isEnableExif;
setState(() {