Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<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" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the log export implementation
find . -type f -name "*.dart" -o -name "*.java" -o -name "*.kt" | head -20

Repository: MostroP2P/mobile

Length of output: 814


🏁 Script executed:

#!/bin/bash
# Search for log export functionality
rg -l "log.*export|export.*log|Downloads" --type dart lib/ | head -20

Repository: MostroP2P/mobile

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Read the logs_screen.dart file
cat -n lib/features/logs/screens/logs_screen.dart | head -100

Repository: MostroP2P/mobile

Length of output: 3686


🏁 Script executed:

#!/bin/bash
# Continue reading the logs_screen.dart file - look for export function
cat -n lib/features/logs/screens/logs_screen.dart | grep -A 50 "export\|_export\|share"

Repository: MostroP2P/mobile

Length of output: 5491


🏁 Script executed:

#!/bin/bash
# Find the _saveToDocuments and _createLogFile functions
cat -n lib/features/logs/screens/logs_screen.dart | grep -A 20 "_saveToDocuments\|_createLogFile"

Repository: MostroP2P/mobile

Length of output: 3260


🏁 Script executed:

#!/bin/bash
# Get the rest of _saveToDocuments function
cat -n lib/features/logs/screens/logs_screen.dart | sed -n '597,650p'

Repository: MostroP2P/mobile

Length of output: 2342


The manifest permission is correct but the implementation has critical Android 13+ compatibility issues.

The WRITE_EXTERNAL_STORAGE permission with maxSdkVersion="32" correctly limits the legacy permission to Android 12L and below. However, the log export implementation has a critical flaw for Android 13+ devices.

The _shareLogs() function works correctly—it uses FileProvider to securely provide access to files across app boundaries on Android, which is compatible with all Android versions.

The _saveToDevice() function will fail on Android 13+ (API 33+). The code:

  • Uses getExternalStorageDirectory() with manual path string manipulation to access the Downloads folder
  • Performs direct file writes to public storage directories
  • Does not use scoped storage APIs, which apps targeting API 30+ must use

Direct access to shared/external storage is deprecated; when an app targets API level 29+, paths returned by getExternalStorageDirectory() are no longer directly accessible. On Android 11+, MediaProvider becomes the file system handler for external storage via FUSE and can intercept kernel calls to ensure file operations are privacy-safe. This blocks the direct file operations the current implementation attempts.

Migrate the _saveToDevice() logic to use MediaStore.Downloads collection (available on Android 10+), which requires no storage permissions for files the app creates, or implement version-specific logic that uses the Storage Access Framework for Android 13+.

🤖 Prompt for AI Agents
In android/app/src/main/AndroidManifest.xml around line 66, the manifest
correctly limits WRITE_EXTERNAL_STORAGE with maxSdkVersion="32", but the app's
_saveToDevice() implementation uses getExternalStorageDirectory() and direct
file writes which will fail on Android 11+ (and Android 13/API33+) due to scoped
storage; update _saveToDevice() to use MediaStore.Downloads (insert via
ContentResolver with proper ContentValues) for Android 10+ so you can save files
to Downloads without storage permission, and implement a versioned fallback that
uses the Storage Access Framework (ACTION_CREATE_DOCUMENT / SAF) for devices
where MediaStore is not appropriate or when targeting stricter scopes; keep
_shareLogs() using FileProvider unchanged and remove any reliance on legacy
external storage paths or WRITE_EXTERNAL_STORAGE for API 30+.


<queries>
<intent>
Expand Down
15 changes: 14 additions & 1 deletion lib/background/background.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
Expand All @@ -8,13 +9,16 @@ import 'package:mostro_mobile/data/repositories/event_storage.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart' as notification_service;
import 'package:mostro_mobile/services/nostr_service.dart';
import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';

bool isAppForeground = true;
String currentLanguage = 'en';

@pragma('vm:entry-point')
Future<void> serviceMain(ServiceInstance service) async {
SendPort? loggerSendPort;
late Logger logger;

final Map<String, Map<String, dynamic>> activeSubscriptions = {};
final nostrService = NostrService();
Expand All @@ -31,6 +35,15 @@ Future<void> serviceMain(ServiceInstance service) async {
final settingsMap = data['settings'];
if (settingsMap == null) return;

loggerSendPort = data['loggerSendPort'] as SendPort?;

// Create logger that forwards to main thread
logger = Logger(
printer: logger_service.SimplePrinter(),
output: logger_service.IsolateLogOutput(loggerSendPort),
level: Level.debug,
);

final settings = Settings.fromJson(settingsMap);
currentLanguage = settings.selectedLanguage ?? PlatformDispatcher.instance.locale.languageCode;
await nostrService.init(settings);
Expand Down Expand Up @@ -74,7 +87,7 @@ Future<void> serviceMain(ServiceInstance service) async {
}
await notification_service.retryNotification(event);
} catch (e) {
Logger().e('Error processing event', error: e);
logger.e('Error processing event', error: e);
}
});
});
Expand Down
10 changes: 9 additions & 1 deletion lib/background/desktop_background_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/nostr_filter.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'abstract_background_service.dart';

class DesktopBackgroundService implements BackgroundService {
Expand All @@ -22,13 +23,20 @@ class DesktopBackgroundService implements BackgroundService {
final isolateReceivePort = ReceivePort();
final mainSendPort = args[0] as SendPort;
final token = args[1] as RootIsolateToken;
final loggerSendPort = args.length > 2 ? args[2] as SendPort? : null; // Optional logger SendPort

mainSendPort.send(isolateReceivePort.sendPort);

BackgroundIsolateBinaryMessenger.ensureInitialized(token);

final logger = Logger(
printer: logger_service.SimplePrinter(),
output: logger_service.IsolateLogOutput(loggerSendPort),
level: Level.debug,
);

final nostrService = NostrService();
final logger = Logger();

bool isAppForeground = true;

isolateReceivePort.listen((message) async {
Expand Down
19 changes: 10 additions & 9 deletions lib/background/mobile_background_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'dart:async';

import 'package:dart_nostr/nostr/model/request/filter.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'package:mostro_mobile/background/background.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'abstract_background_service.dart';
Expand All @@ -16,7 +16,7 @@ class MobileBackgroundService implements BackgroundService {

final _subscriptions = <String, Map<String, dynamic>>{};
bool _isRunning = false;
final _logger = Logger();

bool _serviceReady = false;
final List<Function> _pendingOperations = [];

Expand Down Expand Up @@ -45,18 +45,18 @@ class MobileBackgroundService implements BackgroundService {
service.invoke('start', {
'settings': _settings.toJson(),
});
_logger.d(
logger_service.logger.d(
'Service started with settings: ${_settings.toJson()}',
);
});

service.on('on-stop').listen((event) {
_isRunning = false;
_logger.i('Service stopped');
logger_service.logger.i('Service stopped');
});

service.on('service-ready').listen((data) {
_logger.i("Service confirmed it's ready");
logger_service.logger.i("Service confirmed it's ready");
_serviceReady = true;
_processPendingOperations();
});
Expand All @@ -68,7 +68,7 @@ class MobileBackgroundService implements BackgroundService {
_subscriptions[subId] = {'filters': filters};

_executeWhenReady(() {
_logger.i("Sending subscription to service");
logger_service.logger.i("Sending subscription to service");
service.invoke('create-subscription', {
'id': subId,
'filters': filters.map((f) => f.toMap()).toList(),
Expand Down Expand Up @@ -127,7 +127,7 @@ class MobileBackgroundService implements BackgroundService {
try {
await _startService();
} catch (e) {
_logger.e('Error starting service: $e');
logger_service.logger.e('Error starting service: $e');
// Retry with a delay if needed
await Future.delayed(Duration(seconds: 1));
await _startService();
Expand All @@ -137,7 +137,7 @@ class MobileBackgroundService implements BackgroundService {
}

Future<void> _startService() async {
_logger.i("Starting service");
logger_service.logger.i("Starting service");
await service.startService();
_serviceReady = false; // Reset ready state when starting

Expand All @@ -152,9 +152,10 @@ class MobileBackgroundService implements BackgroundService {
await Future.delayed(const Duration(milliseconds: 50));
}

_logger.i("Service running, sending settings");
logger_service.logger.i("Service running, sending settings");
service.invoke('start', {
'settings': _settings.toJson(),
'loggerSendPort': logger_service.isolateLogSenderPort,
});
}

Expand Down
14 changes: 12 additions & 2 deletions lib/core/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import 'package:mostro_mobile/features/walkthrough/screens/walkthrough_screen.da
import 'package:mostro_mobile/features/disputes/screens/dispute_chat_screen.dart';

import 'package:mostro_mobile/features/notifications/screens/notifications_screen.dart';
import 'package:mostro_mobile/features/logs/screens/logs_screen.dart';

import 'package:mostro_mobile/features/walkthrough/providers/first_run_provider.dart';
import 'package:mostro_mobile/shared/widgets/navigation_listener_widget.dart';
import 'package:mostro_mobile/shared/widgets/notification_listener_widget.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/generated/l10n.dart';

GoRouter createRouter(WidgetRef ref) {
Expand Down Expand Up @@ -59,7 +60,7 @@ GoRouter createRouter(WidgetRef ref) {
);
},
errorBuilder: (context, state) {
final logger = Logger();

logger.w('GoRouter error: ${state.error}');

// For errors, show a generic error page
Expand Down Expand Up @@ -284,6 +285,15 @@ GoRouter createRouter(WidgetRef ref) {
child: const NotificationsScreen(),
),
),
GoRoute(
path: '/logs',
pageBuilder: (context, state) =>
buildPageWithDefaultTransition<void>(
context: context,
state: state,
child: const LogsScreen(),
),
),
],
),
],
Expand Down
5 changes: 5 additions & 0 deletions lib/core/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ class Config {
// Notification configuration
static String notificationChannelId = 'mostro_mobile';
static int notificationId = 38383;

// Logger configuration
static const int logMaxEntries = 1000;
static const int logBatchDeleteSize = 100;
static bool fullLogsInfo = true; // false = simple logs in console, true = full Logger format in console
}
21 changes: 10 additions & 11 deletions lib/core/deep_link_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/generated/l10n.dart';
import 'package:mostro_mobile/services/deep_link_service.dart';
import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart';

class DeepLinkHandler {
final Ref _ref;
final Logger _logger = Logger();
StreamSubscription<Uri>? _subscription;

DeepLinkHandler(this._ref);

/// Initializes deep link handling for the app
void initialize(GoRouter router) {
_logger.i('Initializing DeepLinkHandler');
logger.i('Initializing DeepLinkHandler');

// Get the deep link service instance
final deepLinkService = _ref.read(deepLinkServiceProvider);
Expand All @@ -27,7 +26,7 @@ class DeepLinkHandler {
// Listen for deep link events
_subscription = deepLinkService.deepLinkStream.listen(
(Uri uri) => _handleDeepLink(uri, router),
onError: (error) => _logger.e('Deep link stream error: $error'),
onError: (error) => logger.e('Deep link stream error: $error'),
);
}

Expand All @@ -42,20 +41,20 @@ class DeepLinkHandler {
GoRouter router,
) async {
try {
_logger.i('Handling deep link: $uri');
logger.i('Handling deep link: $uri');

// Check if it's a mostro: scheme
if (uri.scheme == 'mostro') {
await _handleMostroDeepLink(uri.toString(), router);
} else {
_logger.w('Unsupported deep link scheme: ${uri.scheme}');
logger.w('Unsupported deep link scheme: ${uri.scheme}');
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null && context.mounted) {
_showErrorSnackBar(context, S.of(context)!.unsupportedLinkFormat);
}
}
} catch (e) {
_logger.e('Error handling deep link: $e');
logger.e('Error handling deep link: $e');
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null && context.mounted) {
_showErrorSnackBar(context, S.of(context)!.failedToOpenLink);
Expand Down Expand Up @@ -83,7 +82,7 @@ class DeepLinkHandler {
// Ensure we have a valid context for processing
final processingContext = context ?? router.routerDelegate.navigatorKey.currentContext;
if (processingContext == null || !processingContext.mounted) {
_logger.e('No valid context available for deep link processing');
logger.e('No valid context available for deep link processing');
return;
}

Expand All @@ -103,17 +102,17 @@ class DeepLinkHandler {
WidgetsBinding.instance.addPostFrameCallback((_) {
deepLinkService.navigateToOrder(router, result.orderInfo!);
});
_logger.i('Successfully navigated to order: ${result.orderInfo!.orderId} (${result.orderInfo!.orderType.value})');
logger.i('Successfully navigated to order: ${result.orderInfo!.orderId} (${result.orderInfo!.orderType.value})');
} else {
final errorContext = router.routerDelegate.navigatorKey.currentContext;
if (errorContext != null && errorContext.mounted) {
final errorMessage = result.error ?? S.of(errorContext)!.failedToLoadOrder;
_showErrorSnackBar(errorContext, errorMessage);
}
_logger.w('Failed to process mostro link: ${result.error}');
logger.w('Failed to process mostro link: ${result.error}');
}
} catch (e) {
_logger.e('Error processing mostro deep link: $e');
logger.e('Error processing mostro deep link: $e');
final errorContext = router.routerDelegate.navigatorKey.currentContext;
if (errorContext != null && errorContext.mounted) {
Navigator.of(errorContext).pop(); // Hide loading if still showing
Expand Down
21 changes: 10 additions & 11 deletions lib/core/deep_link_interceptor.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/services/logger_service.dart';

/// A deep link interceptor that prevents custom schemes from reaching GoRouter
/// This prevents assertion failures when the system tries to parse mostro: URLs
class DeepLinkInterceptor extends WidgetsBindingObserver {
final Logger _logger = Logger();
final StreamController<String> _customUrlController =
final StreamController<String> _customUrlController =
StreamController<String>.broadcast();

/// Stream for custom URLs that were intercepted
Expand All @@ -15,17 +14,17 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
/// Initialize the interceptor
void initialize() {
WidgetsBinding.instance.addObserver(this);
_logger.i('DeepLinkInterceptor initialized');
logger.i('DeepLinkInterceptor initialized');
}

@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
final uri = routeInformation.uri;
_logger.i('DeepLinkInterceptor: Route information received: $uri');
logger.i('DeepLinkInterceptor: Route information received: $uri');

// Check if this is a custom scheme URL
if (_isCustomScheme(uri)) {
_logger.i('DeepLinkInterceptor: Custom scheme detected: ${uri.scheme}, intercepting and preventing GoRouter processing');
logger.i('DeepLinkInterceptor: Custom scheme detected: ${uri.scheme}, intercepting and preventing GoRouter processing');

// Emit the custom URL for processing
_customUrlController.add(uri.toString());
Expand All @@ -35,7 +34,7 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
return true;
}

_logger.i('DeepLinkInterceptor: Allowing normal URL to pass through: $uri');
logger.i('DeepLinkInterceptor: Allowing normal URL to pass through: $uri');
// Let normal URLs pass through to GoRouter
return super.didPushRouteInformation(routeInformation);
}
Expand All @@ -45,17 +44,17 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
@override
// ignore: deprecated_member_use
Future<bool> didPushRoute(String route) async {
_logger.i('DeepLinkInterceptor: didPushRoute called with: $route');
logger.i('DeepLinkInterceptor: didPushRoute called with: $route');

try {
final uri = Uri.parse(route);
if (_isCustomScheme(uri)) {
_logger.i('DeepLinkInterceptor: Custom scheme detected in didPushRoute: ${uri.scheme}, intercepting');
logger.i('DeepLinkInterceptor: Custom scheme detected in didPushRoute: ${uri.scheme}, intercepting');
_customUrlController.add(route);
return true;
}
} catch (e) {
_logger.w('DeepLinkInterceptor: Error parsing route in didPushRoute: $e');
logger.w('DeepLinkInterceptor: Error parsing route in didPushRoute: $e');
}

// ignore: deprecated_member_use
Expand All @@ -72,6 +71,6 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_customUrlController.close();
_logger.i('DeepLinkInterceptor disposed');
logger.i('DeepLinkInterceptor disposed');
}
}
Loading