5 Commits

Author SHA1 Message Date
Mikkeli Matlock f9037a6697 v2.2.2: Enhanced background persistence and overlay communication
Major improvements to floating window functionality:
- Implemented lazy overlay listener creation/destruction to eliminate ghost overlay
- Added 2Hz cached GPS data push for smooth overlay updates
- Enhanced background persistence with wake locks and heartbeat mechanisms
- Proper Android foreground functionality using bg_launcher package
- Fixed overlay state management race conditions
- Added comprehensive debug logging throughout communication system
- Improved Android manifest with background persistence permissions
- Eliminated overlay status polling interference with data flow

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 23:36:11 +09:00
Mikkeli Matlock bd594601f1 v2.2.0: Fix floating window sizing and state synchronization
- Fix overlay size calculation to use physical pixels instead of scaled logical pixels
- Add proper state synchronization between main app and overlay for close events
- Improve overlay close button functionality with bidirectional communication
- Add comprehensive debug output for screen size analysis
- Update overlay visual tweaks (close button sizing, speed display improvements)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 00:14:41 +09:00
Mikkeli Matlock 2910706e87 CRITICAL FIX: Resolve StackOverflow in debug function
Fixed infinite recursion in customDebugPrint function:
- Line 15 was calling customDebugPrint(message) instead of print(message)
- This caused stack overflow crash on any debug output
- Now correctly calls print(message) to avoid recursion

The app was completely broken due to this recursive function bug.
Debug output now works correctly without crashing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 23:48:37 +09:00
Mikkeli Matlock d1d7521805 floating overlay size change 2025-08-26 23:41:12 +09:00
Mikkeli Matlock 3e79a448ec Make debug output production-ready with conditional printing
- Add kDebugMode conditional debug output using debugPrint() helper
- All debug messages (🚀🟢🔴📡📤📐🖱️) only appear in debug builds
- Release builds will be clean without console spam
- Maintains full debug visibility during development

This ensures production APKs have no debug console output while
preserving comprehensive debugging capabilities for development.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 23:40:30 +09:00
4 changed files with 398 additions and 106 deletions
+11 -2
View File
@@ -10,10 +10,17 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- Background persistence permissions -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application <application
android:label="Speedo" android:label="Speedo"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:killAfterRestore="false"
android:largeHeap="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -22,7 +29,9 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize"
android:excludeFromRecents="false"
android:stateNotNeeded="false">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
+375 -103
View File
@@ -1,13 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // For kDebugMode
import 'package:flutter/services.dart'; // For HapticFeedback
import 'dart:async'; import 'dart:async';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart'; import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:bg_launcher/bg_launcher.dart'; // For bringing app to foreground
import 'speed_units.dart'; import 'speed_units.dart';
import 'gps_service.dart'; import 'gps_service.dart';
import 'color_themes.dart'; import 'color_themes.dart';
import 'dart:math' as math; // Import for math.pi import 'dart:math' as math; // Import for math.pi
// Global debug configuration
const bool _debugRandomValues = false; // Set to true for indoor testing with random values, false for 2Hz cached real GPS
// Debug helper - only prints in debug builds
void customDebugPrint(String message) {
if (kDebugMode) {
print(message);
}
}
void main() { void main() {
runApp(const SpeedoApp()); runApp(const SpeedoApp());
} }
@@ -15,7 +28,7 @@ void main() {
// Entry point for overlay window // Entry point for overlay window
@pragma("vm:entry-point") @pragma("vm:entry-point")
void overlayMain() { void overlayMain() {
print('[Overlay] 🚀 overlayMain() called'); customDebugPrint('[Overlay] 🚀 overlayMain() called');
runApp(const MaterialApp( runApp(const MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: OverlaySpeedometer(), home: OverlaySpeedometer(),
@@ -54,25 +67,152 @@ class SpeedometerScreen extends StatefulWidget {
class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindingObserver { class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindingObserver {
double _speed = 0.0; double _speed = 0.0;
double _heading = -1.0; double _heading = -1.0;
double _realSpeed = 0.0; // Store authentic GPS values
double _realHeading = -1.0;
SpeedUnit _currentUnit = SpeedUnit.kmh; SpeedUnit _currentUnit = SpeedUnit.kmh;
int _currentThemeIndex = 0; int _currentThemeIndex = 0;
StreamSubscription<Position>? _positionSubscription; StreamSubscription<Position>? _positionSubscription;
StreamSubscription<dynamic>? _overlaySubscription;
String _errorMessage = ''; String _errorMessage = '';
bool _isInBackground = false; bool _isInBackground = false;
bool _isOverlayActive = false; bool _isOverlayActive = false;
// Single source of truth for overlay sizing
Map<String, int> _getOverlaySize() {
final window = WidgetsBinding.instance.platformDispatcher.views.first;
final physicalSize = window.physicalSize;
final devicePixelRatio = window.devicePixelRatio;
final systemSize = physicalSize / devicePixelRatio;
customDebugPrint('[Main] 🔍 Screen size analysis:');
customDebugPrint(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}');
customDebugPrint(' devicePixelRatio: $devicePixelRatio');
customDebugPrint(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}');
// Use actual physical pixels, not scaled logical pixels
final overlayWidth = (physicalSize.width * 0.45).round(); // 40% of actual screen width
final overlayHeight = (overlayWidth * 0.6).round();
customDebugPrint(' using fixed overlay size: ${overlayWidth}x$overlayHeight');
return {'width': overlayWidth, 'height': overlayHeight};
}
Timer? _backgroundHeartbeatTimer;
Timer? _overlayDataPushTimer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_initializeGps(); _initializeGps();
_enableWakelock(); _enableWakelock();
_ensureOverlayPermission();
// Overlay listener will be created lazily when overlay is shown
// _startOverlayStatusPolling(); // Disabled - was interfering with data flow by resetting _isOverlayActive
_startBackgroundHeartbeat();
}
void _startListeningToOverlayMessages() {
// Only create listener when overlay is actually shown
if (_overlaySubscription != null) {
customDebugPrint('[Main] ⚠️ Overlay listener already exists, skipping creation');
return;
}
customDebugPrint('[Main] 🎧 Starting overlay message listener');
_overlaySubscription = FlutterOverlayWindow.overlayListener.listen((data) {
customDebugPrint('[Main] 📡 Received message from overlay: $data');
if (data is Map) {
switch (data['action']) {
case 'overlayClosed':
customDebugPrint('[Main] ✅ Overlay close notification received');
_handleOverlayClose();
break;
case 'bringToFront':
customDebugPrint('[Main] 🚀 Bring to front request received');
_bringAppToFront();
break;
default:
customDebugPrint('[Main] ⚠️ Unknown overlay action: ${data['action']}');
}
}
});
}
void _stopListeningToOverlayMessages() {
customDebugPrint('[Main] 🔇 Stopping overlay message listener');
_overlaySubscription?.cancel();
_overlaySubscription = null;
}
void _handleOverlayClose() {
setState(() {
_isOverlayActive = false;
});
_stopListeningToOverlayMessages(); // Clean up listener when overlay closes
}
void _startBackgroundHeartbeat() {
// Aggressive heartbeat to keep main app process active for overlay communication
_backgroundHeartbeatTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (_isInBackground && _isOverlayActive) {
customDebugPrint('[Main] 💓 Background heartbeat - keeping process alive for overlay');
// Minimal activity to prevent hibernation
if (mounted) {
// Force data push to overlay to maintain communication
_pushDataToOverlay();
// Small state update to keep Flutter engine active
setState(() {
// Tiny update that doesn't affect UI but keeps engine alive
_isInBackground = _isInBackground;
});
}
} else if (_isInBackground) {
customDebugPrint('[Main] 💤 Background heartbeat - no overlay, lighter activity');
if (mounted) {
// Lighter heartbeat when no overlay is active
setState(() {
_isInBackground = _isInBackground;
});
}
}
});
}
void _bringAppToFront() {
try {
// Use proper Android method to bring app to foreground
BgLauncher.bringAppToForeground();
customDebugPrint('[Main] ✅ App brought to front via BgLauncher');
if (mounted) {
setState(() {});
}
} catch (e) {
customDebugPrint('[Main] ❌ Failed to bring app to front: $e');
// Fallback to simple state update (original method)
if (mounted) {
setState(() {});
}
}
} }
Future<void> _enableWakelock() async { Future<void> _enableWakelock() async {
try { try {
// Force enable wake lock even if already enabled for robustness
await WakelockPlus.enable(); await WakelockPlus.enable();
final isEnabled = await WakelockPlus.enabled;
customDebugPrint('[Main] 🔓 Wake lock enabled: $isEnabled - keeping app alive');
// Double-check wake lock status for debugging
if (!isEnabled) {
customDebugPrint('[Main] ⚠️ Wake lock not properly enabled, retrying...');
await WakelockPlus.enable();
}
} catch (e) { } catch (e) {
customDebugPrint('[Main] ❌ Wake lock failed: $e');
// Wake lock not supported on this platform, continue normally // Wake lock not supported on this platform, continue normally
} }
} }
@@ -81,7 +221,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
void dispose() { void dispose() {
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
_positionSubscription?.cancel(); _positionSubscription?.cancel();
_overlaySubscription?.cancel();
_backgroundHeartbeatTimer?.cancel();
_overlayDataPushTimer?.cancel();
WakelockPlus.disable(); WakelockPlus.disable();
customDebugPrint('[Main] 🛑 Main app disposed - all subscriptions and timers canceled');
super.dispose(); super.dispose();
} }
@@ -109,14 +253,57 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
} }
void _handleBackgroundTransition() { void _handleBackgroundTransition() {
// When app goes to background, show floating window if GPS is active and no error // Enhanced background persistence
// Temporarily disable auto-trigger for debugging customDebugPrint('[Main] 🌙 App going to background - activating persistence measures');
// if (_speed >= 0 && _errorMessage.isEmpty) {
// _showFloatingWindow(); // Keep GPS stream active
// } if (_positionSubscription != null) {
customDebugPrint('[Main] 📍 GPS stream staying active in background');
}
// Ensure wake lock stays active
_enableWakelock();
// Request battery optimization exemption for better background persistence
_requestBatteryOptimizationExemption();
// If overlay is active, ensure more frequent data pushing
if (_isOverlayActive) {
customDebugPrint('[Main] 🔄 Overlay active - ensuring continuous data flow');
_pushDataToOverlay();
}
}
Future<void> _requestBatteryOptimizationExemption() async {
try {
// This will show Android's battery optimization whitelist dialog
// Users can manually add the app to prevent aggressive power management
customDebugPrint('[Main] 🔋 Requesting battery optimization exemption for background persistence');
// Note: This requires platform-specific implementation which we'd need to add via method channel
// For now, we'll rely on other persistence mechanisms
} catch (e) {
customDebugPrint('[Main] ⚠️ Battery optimization exemption not available: $e');
}
}
Future<void> _ensureOverlayPermission() async {
try {
// Check if overlay permission is granted (required for bg_launcher to work properly)
final isGranted = await FlutterOverlayWindow.isPermissionGranted();
customDebugPrint('[Main] 🔐 Overlay permission granted: $isGranted');
if (!isGranted) {
customDebugPrint('[Main] 📱 Requesting overlay permission for proper foreground functionality');
final requestResult = await FlutterOverlayWindow.requestPermission();
customDebugPrint('[Main] 📱 Overlay permission request result: $requestResult');
}
} catch (e) {
customDebugPrint('[Main] ❌ Error checking overlay permission: $e');
}
} }
Future<void> _initializeGps() async { Future<void> _initializeGps() async {
// Always initialize real GPS for authentic behavior
try { try {
final serviceEnabled = await GpsService.isLocationServiceEnabled(); final serviceEnabled = await GpsService.isLocationServiceEnabled();
if (!serviceEnabled) { if (!serviceEnabled) {
@@ -140,42 +327,102 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
} }
}, },
); );
customDebugPrint('[Main] 📡 Real GPS initialized');
} catch (e) { } catch (e) {
setState(() => _errorMessage = 'Failed to initialize GPS'); setState(() => _errorMessage = 'Failed to initialize GPS');
} }
// Start 2Hz overlay data push timer (debug mode or cached real GPS)
if (kDebugMode) {
if (_debugRandomValues) {
customDebugPrint('[Main] 🎲 DEBUG MODE: Starting random value overlay for indoor testing');
} else {
customDebugPrint('[Main] 📡 NORMAL MODE: Starting 2Hz cached GPS push for smooth overlay updates');
}
_start2HzOverlayDataPush(); // Function handles both debug and normal modes
}
} }
void _onPositionUpdate(Position position) { void _onPositionUpdate(Position position) {
setState(() { setState(() {
_speed = position.speed; // Always store real GPS values
_heading = position.heading; _realSpeed = position.speed;
_realHeading = position.heading;
// Use real values for display (debug mode will override these)
_speed = _realSpeed;
_heading = _realHeading;
_errorMessage = ''; _errorMessage = '';
}); });
// Push display data to overlay if active customDebugPrint('[Main] 📡 Real GPS: ${_realSpeed.toStringAsFixed(1)} m/s, ${_realHeading.toStringAsFixed(1)}°');
_pushDataToOverlay();
// Always push display data to overlay if active - critical for background communication
if (_isOverlayActive) {
_pushDataToOverlay();
customDebugPrint('[Main] 📤 Data pushed to overlay from GPS update');
}
}
void _start2HzOverlayDataPush() {
// 2Hz data push timer: either cached real GPS for smooth overlay updates, or random values for debug
final random = math.Random();
_overlayDataPushTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (_debugRandomValues) {
// DEBUG MODE: Generate random values for indoor testing
final debugSpeedKmh = random.nextDouble() * 80.0;
final debugSpeed = debugSpeedKmh / 3.6; // Convert to m/s like real GPS
final debugHeading = random.nextDouble() * 360.0;
setState(() {
// Override display values only (real GPS values preserved in _realSpeed/_realHeading)
_speed = debugSpeed;
_heading = debugHeading;
_errorMessage = ''; // Clear errors for debug mode
});
customDebugPrint('[Main] 🎲 DEBUG override: ${debugSpeedKmh.toStringAsFixed(1)} km/h, ${debugHeading.toStringAsFixed(1)}° (real GPS still running)');
} else {
// NORMAL MODE: Push cached real GPS data at consistent 2Hz for smooth overlay updates
setState(() {
// Use cached real GPS values for display
_speed = _realSpeed;
_heading = _realHeading;
});
customDebugPrint('[Main] 📡 2Hz GPS push: ${(_currentUnit.convert(_realSpeed)).toStringAsFixed(1)} ${_currentUnit.label}, ${_realHeading.toStringAsFixed(1)}° (cached real GPS)');
}
// Always push converted display data to overlay (2Hz timer ensures consistent flow)
// Don't check _isOverlayActive here since this timer only runs in debug mode for consistent overlay updates
_pushDataToOverlay(); // This function handles unit conversion properly
customDebugPrint('[Main] 📤 ${_debugRandomValues ? "Debug" : "Cached GPS"} display data (${_currentUnit.label}) pushed to overlay (2Hz timer)');
});
customDebugPrint('[Main] ✅ ${_debugRandomValues ? "Debug random values" : "2Hz GPS cache push"} timer started - real GPS continues in background');
} }
void _pushDataToOverlay() { void _pushDataToOverlay() {
// Only push data if overlay is actually active (listener check was causing race conditions)
if (!_isOverlayActive) {
customDebugPrint('[Main] 🚫 Skipping data push - no active overlay (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})');
return;
}
customDebugPrint('[Main] ✅ Overlay guard passed - pushing data (active: $_isOverlayActive, subscription: ${_overlaySubscription != null})');
// Send complete display data to overlay including screen size // Send complete display data to overlay including screen size
final displaySpeed = _currentUnit.convert(_speed); final displaySpeed = _currentUnit.convert(_speed);
final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString(); final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString();
final headingText = GpsService.formatHeading(_heading); final headingText = GpsService.formatHeading(_heading);
// Get system screen size to share with overlay customDebugPrint('[Main] 📤 SENDING to overlay:');
final window = WidgetsBinding.instance.platformDispatcher.views.first; customDebugPrint(' speedText: "$speedText"');
final systemSize = window.physicalSize / window.devicePixelRatio; customDebugPrint(' headingText: "$headingText"');
customDebugPrint(' direction: ${_heading.toStringAsFixed(1)}°');
print('[Main] 📤 SENDING to overlay:'); customDebugPrint(' unit: ${_currentUnit.label}, theme: $_currentThemeIndex');
print(' speedText: "$speedText"');
print(' headingText: "$headingText"');
print(' direction: ${_heading.toStringAsFixed(1)}°');
print(' unit: ${_currentUnit.label}, theme: $_currentThemeIndex');
print(' systemScreen: ${systemSize.width.round()}x${systemSize.height.round()}');
// Calculate overlay size the same way as when launching
final overlayWidth = (systemSize.width * 0.4).round();
final overlayHeight = (overlayWidth * 0.5).round();
FlutterOverlayWindow.shareData({ FlutterOverlayWindow.shareData({
'action': 'updateDisplay', 'action': 'updateDisplay',
@@ -184,10 +431,6 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
'heading': _heading, 'heading': _heading,
'unitIndex': _currentUnit.index, 'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex, 'themeIndex': _currentThemeIndex,
'systemScreenWidth': systemSize.width,
'systemScreenHeight': systemSize.height,
'overlayWidth': overlayWidth.clamp(280, 480).toDouble(),
'overlayHeight': overlayHeight.clamp(140, 240).toDouble(),
}); });
} }
@@ -209,17 +452,10 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
Future<void> _showFloatingWindow() async { Future<void> _showFloatingWindow() async {
try { try {
// Get actual system screen resolution for proportional sizing // Use centralized sizing function
final window = WidgetsBinding.instance.platformDispatcher.views.first; final overlaySize = _getOverlaySize();
final systemSize = window.physicalSize / window.devicePixelRatio;
final overlayWidth = (systemSize.width * 0.4).round(); // 40% of system screen width
final overlayHeight = (overlayWidth * 0.5).round(); // 2:1 aspect ratio
// Debug info for development // Send initial display data WITH size info (only needed on creation)
// print('[Main] System resolution: ${systemSize.width.round()}x${systemSize.height.round()}');
// print('[Main] Calculated overlay size: ${overlayWidth}x${overlayHeight}');
// Send initial display data to overlay including screen size
final displaySpeed = _currentUnit.convert(_speed); final displaySpeed = _currentUnit.convert(_speed);
final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString(); final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString();
final headingText = GpsService.formatHeading(_heading); final headingText = GpsService.formatHeading(_heading);
@@ -231,29 +467,34 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
'heading': _heading, 'heading': _heading,
'unitIndex': _currentUnit.index, 'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex, 'themeIndex': _currentThemeIndex,
'systemScreenWidth': systemSize.width, 'overlayWidth': overlaySize['width']!.toDouble(),
'systemScreenHeight': systemSize.height, 'overlayHeight': overlaySize['height']!.toDouble(),
'overlayWidth': overlayWidth.clamp(280, 480).toDouble(),
'overlayHeight': overlayHeight.clamp(140, 240).toDouble(),
}); });
// Show the overlay with proportional sizing based on system resolution // Show the overlay with proportional sizing based on system resolution
await FlutterOverlayWindow.showOverlay( await FlutterOverlayWindow.showOverlay(
enableDrag: true, enableDrag: true,
overlayTitle: "GPS Speedometer", overlayTitle: "Speedometer",
overlayContent: 'GPS Speedometer Overlay', overlayContent: 'Speedo overlay active',
flag: OverlayFlag.defaultFlag, flag: OverlayFlag.defaultFlag,
visibility: NotificationVisibility.visibilityPublic, visibility: NotificationVisibility.visibilityPublic,
positionGravity: PositionGravity.none, positionGravity: PositionGravity.none,
// Use system-based proportional sizing with reasonable limits // Use consistent sizing from centralized function
width: overlayWidth.clamp(280, 480), width: overlaySize['width']!,
height: overlayHeight.clamp(140, 240), height: overlaySize['height']!,
); );
print('[Main] 🟢 OVERLAY LAUNCHED - Size: ${overlayWidth.clamp(280, 480)}x${overlayHeight.clamp(140, 240)}'); customDebugPrint('[Main] 🟢 OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}');
setState(() {
_isOverlayActive = true; // Set overlay active immediately and synchronously
}); _isOverlayActive = true;
customDebugPrint('[Main] ✅ _isOverlayActive set to: $_isOverlayActive');
// Start listening to overlay messages after state is set
_startListeningToOverlayMessages();
// Force UI update
if (mounted) setState(() {});
} catch (e) { } catch (e) {
// Silent error handling - floating window issues shouldn't crash main app // Silent error handling - floating window issues shouldn't crash main app
} }
@@ -261,18 +502,16 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
Future<void> _closeFloatingWindow() async { Future<void> _closeFloatingWindow() async {
if (!_isOverlayActive) { if (!_isOverlayActive) {
print('[Main] ⚠️ No overlay to close'); customDebugPrint('[Main] ⚠️ No overlay to close');
return; return;
} }
try { try {
await FlutterOverlayWindow.closeOverlay(); await FlutterOverlayWindow.closeOverlay();
print('[Main] 🔴 OVERLAY CLOSED - Requested from main app'); customDebugPrint('[Main] 🔴 OVERLAY CLOSED - Requested from main app');
setState(() { _handleOverlayClose(); // This will set _isOverlayActive = false and stop listener
_isOverlayActive = false;
});
} catch (e) { } catch (e) {
print('[Main] ❌ Error closing overlay: $e'); customDebugPrint('[Main] ❌ Error closing overlay: $e');
} }
} }
@@ -379,7 +618,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
) )
else ...[ else ...[
Flexible( Flexible(
flex: 4, flex: 7,
child: GestureDetector( child: GestureDetector(
onTap: _cycleTheme, // Tap speed to cycle theme onTap: _cycleTheme, // Tap speed to cycle theme
child: FittedBox( child: FittedBox(
@@ -390,6 +629,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
), ),
Flexible( Flexible(
flex: 1, flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
Flexible(
flex: 2,
child: GestureDetector( child: GestureDetector(
onTap: _cycleUnit, // clicking on unit to cycle unit is more intuitive onTap: _cycleUnit, // clicking on unit to cycle unit is more intuitive
child: FittedBox( child: FittedBox(
@@ -405,6 +650,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
), ),
), ),
), ),
Flexible(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
], ],
], ],
), ),
@@ -415,7 +666,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
flex: 32, flex: 32,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
print('[Main] 🖱️ Navigation area tapped - overlay active: $_isOverlayActive'); customDebugPrint('[Main] 🖱️ Navigation area tapped - overlay active: $_isOverlayActive');
if (_isOverlayActive) { if (_isOverlayActive) {
_closeFloatingWindow(); _closeFloatingWindow();
} else { } else {
@@ -423,7 +674,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
} }
}, },
onLongPress: () { onLongPress: () {
print('[Main] 🖱️ Navigation area long-pressed - overlay active: $_isOverlayActive'); customDebugPrint('[Main] 🖱️ Navigation area long-pressed - overlay active: $_isOverlayActive');
_closeFloatingWindow(); _closeFloatingWindow();
}, },
child: Container( child: Container(
@@ -475,7 +726,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
children: [ children: [
// Speed area - takes majority of space, precise tap targets on text only // Speed area - takes majority of space, precise tap targets on text only
Expanded( Expanded(
flex: 66, flex: 60,
child: Container( child: Container(
height: double.infinity, height: double.infinity,
color: Colors.transparent, color: Colors.transparent,
@@ -538,10 +789,10 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
), ),
// Compass area - smaller but fully tappable for floating window // Compass area - smaller but fully tappable for floating window
Expanded( Expanded(
flex: 33, flex: 35,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
print('[Main] 🖱️ Navigation area tapped (landscape) - overlay active: $_isOverlayActive'); customDebugPrint('[Main] 🖱️ Navigation area tapped (landscape) - overlay active: $_isOverlayActive');
if (_isOverlayActive) { if (_isOverlayActive) {
_closeFloatingWindow(); _closeFloatingWindow();
} else { } else {
@@ -549,7 +800,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
} }
}, },
onLongPress: () { onLongPress: () {
print('[Main] 🖱️ Navigation area long-pressed (landscape) - overlay active: $_isOverlayActive'); customDebugPrint('[Main] 🖱️ Navigation area long-pressed (landscape) - overlay active: $_isOverlayActive');
_closeFloatingWindow(); _closeFloatingWindow();
}, },
child: Container( child: Container(
@@ -592,6 +843,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
), ),
), ),
), ),
Expanded(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
], ],
); );
} }
@@ -616,7 +873,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
print('[Overlay] 🟢 CREATED - Floating window initialized'); customDebugPrint('[Overlay] 🟢 CREATED - Floating window initialized');
_getSystemScreenSize(); _getSystemScreenSize();
_listenToMainAppMessages(); _listenToMainAppMessages();
} }
@@ -629,18 +886,18 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
final devicePixelRatio = window.devicePixelRatio; final devicePixelRatio = window.devicePixelRatio;
final systemSize = physicalSize / devicePixelRatio; final systemSize = physicalSize / devicePixelRatio;
print('[Overlay] 🔍 Screen detection:'); customDebugPrint('[Overlay] 🔍 Screen detection:');
print(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}'); customDebugPrint(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}');
print(' devicePixelRatio: $devicePixelRatio'); customDebugPrint(' devicePixelRatio: $devicePixelRatio');
print(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}'); customDebugPrint(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}');
setState(() { setState(() {
_systemScreenWidth = systemSize.width; _systemScreenWidth = systemSize.width;
}); });
print('[Overlay] ✅ _systemScreenWidth set to: ${_systemScreenWidth.round()}'); customDebugPrint('[Overlay] ✅ _systemScreenWidth set to: ${_systemScreenWidth.round()}');
} catch (e) { } catch (e) {
print('[Overlay] ❌ Error getting system screen size: $e'); customDebugPrint('[Overlay] ❌ Error getting system screen size: $e');
// Fallback to reasonable default // Fallback to reasonable default
setState(() { setState(() {
_systemScreenWidth = 400; _systemScreenWidth = 400;
@@ -668,20 +925,20 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
} }
}); });
print('[Overlay] 📡 UPDATE received:'); customDebugPrint('[Overlay] 📡 UPDATE received:');
print(' speedText: "$_speedText"'); customDebugPrint(' speedText: "$_speedText"');
print(' headingText: "$_headingText"'); customDebugPrint(' headingText: "$_headingText"');
print(' direction: ${_heading.toStringAsFixed(1)}°'); customDebugPrint(' direction: ${_heading.toStringAsFixed(1)}°');
print(' themeIndex: $_currentThemeIndex'); customDebugPrint(' themeIndex: $_currentThemeIndex');
print(' systemScreenWidth: ${_systemScreenWidth.round()}'); customDebugPrint(' systemScreenWidth: ${_systemScreenWidth.round()}');
print(' overlaySize from main: ${_overlayWidth.round()}x${_overlayHeight.round()}'); customDebugPrint(' overlaySize from main: ${_overlayWidth.round()}x${_overlayHeight.round()}');
} }
}); });
} }
@override @override
void dispose() { void dispose() {
print('[Overlay] 🔴 DESTROYED - Floating window disposed'); customDebugPrint('[Overlay] 🔴 DESTROYED - Floating window disposed');
// No GPS subscription to cancel - overlay is display-only // No GPS subscription to cancel - overlay is display-only
super.dispose(); super.dispose();
} }
@@ -693,7 +950,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
// Use overlay dimensions provided by main app (not calculated locally) // Use overlay dimensions provided by main app (not calculated locally)
final fontSize = (_overlayWidth * 0.2); // Font size proportional to actual overlay width final fontSize = (_overlayWidth * 0.2); // Font size proportional to actual overlay width
print('[Overlay] 📐 Window size: ${_overlayWidth.round()}x${_overlayHeight.round()}, fontSize: ${fontSize.round()}'); customDebugPrint('[Overlay] 📐 Window size: ${_overlayWidth.round()}x${_overlayHeight.round()}, fontSize: ${fontSize.round()}');
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
@@ -703,10 +960,10 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
height: double.infinity, height: double.infinity,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: currentTheme.background.withOpacity(0.85), color: currentTheme.background.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: currentTheme.speedText.withOpacity(0.3), color: currentTheme.speedText.withValues(alpha: 0.3),
width: 1 width: 1
), ),
), ),
@@ -724,7 +981,7 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
child: FittedBox( child: FittedBox(
fit: BoxFit.contain, fit: BoxFit.contain,
child: Text( child: Text(
_speedText, (_speedText == "--" ? "0" : _speedText),
style: TextStyle( style: TextStyle(
fontSize: fontSize, fontSize: fontSize,
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
@@ -778,28 +1035,43 @@ class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
), ),
], ],
), ),
// Semi-transparent close button (only interaction as per NOTES.txt) // Transparent full-overlay gesture detection layer
Positioned( Positioned.fill(
top: 4,
right: 4,
child: GestureDetector( child: GestureDetector(
onTap: () => FlutterOverlayWindow.closeOverlay(), onLongPress: () async {
customDebugPrint('[Overlay] 🔴 Long press detected - closing overlay');
// Add haptic feedback for long press
try {
await HapticFeedback.mediumImpact();
customDebugPrint('[Overlay] 📳 Haptic feedback triggered');
} catch (e) {
customDebugPrint('[Overlay] ❌ Haptic feedback failed: $e');
}
// Notify main app before closing
try {
// can we base this on handleOverlayClose() or _closeFloatingWindow()?
await FlutterOverlayWindow.shareData({
'action': 'overlayClosed',
});
customDebugPrint('[Overlay] 📤 Close notification sent');
} catch (e) {
customDebugPrint('[Overlay] ❌ Close notification failed: $e');
}
FlutterOverlayWindow.closeOverlay();
},
onTap: () async {
customDebugPrint('[Overlay] 👆 Tap detected - bringing main app to front');
try {
await FlutterOverlayWindow.shareData({
'action': 'bringToFront',
});
customDebugPrint('[Overlay] 📤 Bring-to-front message sent');
} catch (e) {
customDebugPrint('[Overlay] ❌ Bring-to-front message failed: $e');
}
},
child: Container( child: Container(
width: _overlayHeight * 0.2, color: Colors.transparent,
height: _overlayHeight * 0.2,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.6),
borderRadius: BorderRadius.circular(_overlayHeight * 0.1),
border: Border.all(
color: Colors.white.withOpacity(0.8),
width: 1
),
),
child: Icon(
Icons.close,
color: Colors.white.withOpacity(0.9),
size: _overlayHeight * 0.15,
),
), ),
), ),
), ),
+8
View File
@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
bg_launcher:
dependency: "direct main"
description:
name: bg_launcher
sha256: c4c1dfffdcdb2896411a8f2fce8a8fac4712b1664b243b85f69d5347367ea504
url: "https://pub.dev"
source: hosted
version: "0.1.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
+4 -1
View File
@@ -2,7 +2,7 @@ name: gps_speedo
description: "Speedo - A Flutter application." description: "Speedo - A Flutter application."
publish_to: 'none' publish_to: 'none'
version: 2.1.0+2 version: 2.2.0+1
environment: environment:
sdk: ^3.9.0 sdk: ^3.9.0
@@ -24,6 +24,9 @@ dependencies:
# Floating window overlay capability # Floating window overlay capability
flutter_overlay_window: ^0.5.0 flutter_overlay_window: ^0.5.0
# Bring app to foreground from background/overlay
bg_launcher: ^0.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: