-
Notifications
You must be signed in to change notification settings - Fork 16
Feat Issue #316 (logs): add persistent logging system with export option #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f6d0111
065ab14
a3fef38
7fd5b2a
5ca4407
7273bf5
6bcb180
cd6ea6d
3d53d8c
67c92de
b4a8a82
e9b2fca
05e500b
14df3b3
6cb33a6
dd0d75d
e73ffc1
a427929
9a05c74
12c8932
2b413a6
0460f6e
11f0ea5
dafca38
8269c2e
5811f79
06349a4
9841a61
cc92620
5a28533
7a16fd1
9368861
a457c3c
5a96ec8
1428f1d
1cc6d1c
8eeda0b
7475e7c
08a6184
4bcc887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,73 +1,79 @@ | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:tools="http://schemas.android.com/tools"> | ||
| <application | ||
| android:label="Mostro" | ||
| android:name="${applicationName}" | ||
| android:icon="@mipmap/launcher_icon" | ||
| android:allowBackup="false" | ||
| android:fullBackupContent="false" | ||
| android:enableOnBackInvokedCallback="true"> | ||
|
|
||
| <meta-data android:name="flutter_deeplinking_enabled" android:value="false" /> | ||
| xmlns:tools="http://schemas.android.com/tools"> | ||
|
|
||
| <activity | ||
| android:name=".MainActivity" | ||
| android:exported="true" | ||
| android:launchMode="singleTop" | ||
| android:taskAffinity="" | ||
| android:theme="@style/LaunchTheme" | ||
| android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | ||
| android:hardwareAccelerated="true" | ||
| android:windowSoftInputMode="adjustResize"> | ||
| <meta-data | ||
| android:name="io.flutter.embedding.android.NormalTheme" | ||
| android:resource="@style/NormalTheme" /> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| <application | ||
| android:label="Mostro" | ||
| android:name="${applicationName}" | ||
| android:icon="@mipmap/launcher_icon" | ||
| android:allowBackup="false" | ||
| android:fullBackupContent="false" | ||
| android:enableOnBackInvokedCallback="true"> | ||
|
|
||
| <!-- Deep Link Support for mostro: scheme --> | ||
| <intent-filter android:autoVerify="true"> | ||
| <action android:name="android.intent.action.VIEW" /> | ||
| <category android:name="android.intent.category.DEFAULT" /> | ||
| <category android:name="android.intent.category.BROWSABLE" /> | ||
| <data android:scheme="mostro" /> | ||
| </intent-filter> | ||
| </activity> | ||
| <meta-data | ||
| android:name="flutter_deeplinking_enabled" | ||
| android:value="false" /> | ||
|
|
||
| <service | ||
| android:name="id.flutter.flutter_background_service.BackgroundService" | ||
| android:exported="false" | ||
| android:foregroundServiceType="dataSync" | ||
| android:permission="android.permission.FOREGROUND_SERVICE" | ||
| tools:replace="android:exported" /> | ||
| <activity | ||
| android:name=".MainActivity" | ||
| android:exported="true" | ||
| android:launchMode="singleTop" | ||
| android:taskAffinity="" | ||
| android:theme="@style/LaunchTheme" | ||
| android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | ||
| android:hardwareAccelerated="true" | ||
| android:windowSoftInputMode="adjustResize"> | ||
|
|
||
| <receiver | ||
| android:exported="false" | ||
| android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" /> | ||
| <meta-data | ||
| android:name="flutterEmbedding" | ||
| android:value="2" /> | ||
| <meta-data | ||
| android:name="com.google.firebase.messaging.default_notification_icon" | ||
| android:resource="@drawable/ic_bg_service_small" /> | ||
| <meta-data | ||
| android:name="io.flutter.embedding.android.NormalTheme" | ||
| android:resource="@style/NormalTheme" /> | ||
|
|
||
| </application> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
|
|
||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | ||
| <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | ||
| <uses-permission android:name="android.permission.INTERNET" /> | ||
| <!-- Deep Link Support for mostro: scheme --> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.VIEW" /> | ||
| <category android:name="android.intent.category.DEFAULT" /> | ||
| <category android:name="android.intent.category.BROWSABLE" /> | ||
| <data android:scheme="mostro" /> | ||
| </intent-filter> | ||
| </activity> | ||
|
|
||
| <queries> | ||
| <intent> | ||
| <action android:name="android.intent.action.PROCESS_TEXT" /> | ||
| <data android:mimeType="text/plain" /> | ||
| </intent> | ||
| <intent> | ||
| <action android:name="android.intent.action.VIEW" /> | ||
| <data android:scheme="lightning" /> | ||
| </intent> | ||
| </queries> | ||
| <service | ||
| android:name="id.flutter.flutter_background_service.BackgroundService" | ||
| android:exported="false" | ||
| android:foregroundServiceType="dataSync" | ||
| tools:replace="android:exported" /> | ||
|
|
||
| <receiver | ||
| android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" | ||
| android:exported="false" /> | ||
|
|
||
| <meta-data | ||
| android:name="flutterEmbedding" | ||
| android:value="2" /> | ||
|
|
||
| <meta-data | ||
| android:name="com.google.firebase.messaging.default_notification_icon" | ||
| android:resource="@drawable/ic_bg_service_small" /> | ||
|
|
||
| </application> | ||
|
|
||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> | ||
| <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> | ||
| <uses-permission android:name="android.permission.INTERNET" /> | ||
|
|
||
| <queries> | ||
| <intent> | ||
| <action android:name="android.intent.action.PROCESS_TEXT" /> | ||
| <data android:mimeType="text/plain" /> | ||
| </intent> | ||
| <intent> | ||
| <action android:name="android.intent.action.VIEW" /> | ||
| <data android:scheme="lightning" /> | ||
| </intent> | ||
| </queries> | ||
| </manifest> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,109 @@ | ||
| package network.mostro.app | ||
|
|
||
| import android.os.Handler | ||
| import android.os.Looper | ||
| import android.util.Log | ||
| import io.flutter.embedding.android.FlutterActivity | ||
| import io.flutter.embedding.engine.FlutterEngine | ||
| import io.flutter.plugin.common.EventChannel | ||
| import java.io.BufferedReader | ||
| import java.io.InputStreamReader | ||
| import java.util.concurrent.atomic.AtomicBoolean | ||
|
|
||
| class MainActivity: FlutterActivity() | ||
| class MainActivity : FlutterActivity() { | ||
|
|
||
| private val EVENT_CHANNEL = "native_logcat_stream" | ||
| private var logcatProcess: Process? = null | ||
| private val isCapturing = AtomicBoolean(false) | ||
| private val handler = Handler(Looper.getMainLooper()) | ||
|
|
||
| override fun configureFlutterEngine(flutterEngine: FlutterEngine) { | ||
| super.configureFlutterEngine(flutterEngine) | ||
|
|
||
| // EventChannel for automatic native log streaming | ||
| EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL) | ||
| .setStreamHandler(object : EventChannel.StreamHandler { | ||
| override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { | ||
| startLogCapture(events) | ||
| } | ||
|
|
||
| override fun onCancel(arguments: Any?) { | ||
| stopLogCapture() | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| private fun startLogCapture(eventSink: EventChannel.EventSink?) { | ||
| // Use compareAndSet for thread-safe check-and-set operation | ||
| if (!isCapturing.compareAndSet(false, true)) return | ||
|
|
||
| Thread({ | ||
| var reader: BufferedReader? = null | ||
| try { | ||
| // Capture logs only for this app with timestamp | ||
| logcatProcess = Runtime.getRuntime().exec( | ||
| arrayOf( | ||
| "logcat", | ||
| "-v", "time", | ||
| "--pid=${android.os.Process.myPid()}" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. en el comentario del commit especΓfico ya quedΓ³ outdated pero aquΓ estΓ‘, lo pego de nuevo por si acaso: AquΓ introduces una falla de seguridad porque el flag --pid no funciona en Android < 7.0 y es inconsistente en versiones posteriores. En muchos dispositivos esto captura logs de todas las aplicaciones del sistema, incluyendo WhatsApp, bancos, etc. No debe estar capturando logcat sin filtrar sino usar tags especΓficos para la app de mostro |
||
| ) | ||
| ) | ||
|
|
||
| reader = BufferedReader( | ||
| InputStreamReader(logcatProcess?.inputStream) | ||
| ) | ||
|
|
||
| // Use inline assignment in while loop | ||
| while (isCapturing.get()) { | ||
| val line = reader.readLine() ?: break | ||
|
|
||
| if (line.isNotEmpty()) { | ||
| handler.post { | ||
| eventSink?.success(line) | ||
| } | ||
| } | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.e("MostroLogCapture", "Error capturing native logs", e) | ||
| handler.post { | ||
| eventSink?.error("LOGCAT_ERROR", e.message, null) | ||
| } | ||
| } finally { | ||
| reader?.close() | ||
| logcatProcess?.destroy() | ||
| // Forcibly kill if not terminated after brief wait | ||
| try { | ||
| if (logcatProcess?.waitFor(100, java.util.concurrent.TimeUnit.MILLISECONDS) == false) { | ||
| logcatProcess?.destroyForcibly() | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.w("MostroLogCapture", "Error waiting for process termination", e) | ||
| } | ||
| isCapturing.set(false) | ||
| } | ||
| }, "mostro-logcat").start() | ||
| } | ||
|
|
||
| private fun stopLogCapture() { | ||
| isCapturing.set(false) | ||
| logcatProcess?.let { process -> | ||
| process.destroy() | ||
| // Force kill if still alive after brief wait | ||
| Thread { | ||
| try { | ||
| if (!process.waitFor(200, java.util.concurrent.TimeUnit.MILLISECONDS)) { | ||
| process.destroyForcibly() | ||
| } | ||
| } catch (e: Exception) { | ||
| Log.w("MostroLogCapture", "Process cleanup interrupted", e) | ||
| } | ||
| }.start() | ||
| } | ||
| logcatProcess = null | ||
| } | ||
|
|
||
| override fun onDestroy() { | ||
| stopLogCapture() | ||
| super.onDestroy() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import 'dart:collection'; | ||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
| import 'package:mostro_mobile/features/logs/logs_service.dart'; | ||
|
|
||
| // Define possible states for logs | ||
| enum LogsState { | ||
| loading, | ||
| enabled, | ||
| disabled | ||
| } | ||
|
|
||
| // Main service provider (reactive via ChangeNotifier) | ||
| final logsServiceProvider = ChangeNotifierProvider<LogsService>((ref) { | ||
| final service = LogsService(); | ||
| ref.onDispose(() { | ||
| service.dispose(); | ||
| }); | ||
| return service; | ||
| }); | ||
|
|
||
| // Provider for reactive logs list | ||
| final logsProvider = Provider<UnmodifiableListView<String>>((ref) { | ||
| final service = ref.watch(logsServiceProvider); | ||
| return service.logs; | ||
| }); | ||
|
|
||
|
|
||
| // Updated to use LogsState | ||
| final logsEnabledProvider = StateNotifierProvider<LogsEnabledNotifier, LogsState>((ref) { | ||
| final service = ref.watch(logsServiceProvider); | ||
| return LogsEnabledNotifier(service); | ||
| }); | ||
|
|
||
| // Updated to use LogsState | ||
| final nativeLogsEnabledProvider = StateNotifierProvider<NativeLogsEnabledNotifier, LogsState>((ref) { | ||
| final service = ref.watch(logsServiceProvider); | ||
| return NativeLogsEnabledNotifier(service); | ||
| }); | ||
|
|
||
| // Updated LogsEnabledNotifier to use LogsState | ||
| class LogsEnabledNotifier extends StateNotifier<LogsState> { | ||
| final LogsService _logsService; | ||
|
|
||
| LogsEnabledNotifier(this._logsService) : super(LogsState.loading) { | ||
| _loadState(); | ||
| } | ||
|
|
||
| Future<void> _loadState() async { | ||
| final isEnabled = await _logsService.isLogsEnabled(); | ||
| state = isEnabled ? LogsState.enabled : LogsState.disabled; | ||
| } | ||
|
|
||
| Future<void> toggle(bool enabled) async { | ||
| state = LogsState.loading; | ||
| await _logsService.setLogsEnabled(enabled); | ||
| state = enabled ? LogsState.enabled : LogsState.disabled; | ||
| } | ||
|
|
||
| bool get isEnabled => state == LogsState.enabled; | ||
| } | ||
|
|
||
| // Updated NativeLogsEnabledNotifier to use LogsState | ||
| class NativeLogsEnabledNotifier extends StateNotifier<LogsState> { | ||
| final LogsService _logsService; | ||
|
|
||
| NativeLogsEnabledNotifier(this._logsService) : super(LogsState.loading) { | ||
| _loadState(); | ||
| } | ||
|
|
||
| Future<void> _loadState() async { | ||
| final isEnabled = await _logsService.isNativeLogsEnabled(); | ||
| state = isEnabled ? LogsState.enabled : LogsState.disabled; | ||
| } | ||
|
|
||
| Future<void> toggle(bool enabled) async { | ||
| state = LogsState.loading; | ||
| await _logsService.setNativeLogsEnabled(enabled); | ||
| state = enabled ? LogsState.enabled : LogsState.disabled; | ||
| } | ||
|
|
||
| bool get isEnabled => state == LogsState.enabled; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Delgado74 por quΓ© es necesario cambiar el manifest? me puedes comentar la ventaja de realizar estos cambios?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fue un error de concepto a la hora de implementar el PR. Incluso hay muchas modificaciones. AsΓ que sobre la base de las recomendaciones del equipo estoy trabajando ahora en una soluciΓ³n que solo introduzca 3 archivos nuevos y solo modifica 3 archivos. Creando un PR mΓ‘s sencillo. Gracias por la oportunidad