11 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
Mikkeli Matlock 7ae660156e Complete working floating window implementation
Major improvements to floating window overlay:

Architecture:
- Clean client-server model: main app as GPS backend, overlay as display client
- Single GPS stream eliminates resource conflicts and sync issues
- Real-time data push: speed, heading, theme, and unit changes sync instantly

Functionality:
- Proper system screen resolution detection and size synchronization
- Main app calculates overlay size, shares exact dimensions with overlay
- Navigation area tap toggles overlay (show/close), long-press always closes
- Comprehensive debug metrics for troubleshooting

Technical fixes:
- Eliminated duplicate GPS streams for better battery life
- Fixed size calculation mismatches between main app and overlay
- Proper overlay state tracking with _isOverlayActive boolean
- Reverted to withOpacity() per NOTES.txt (simpler than Color.fromARGB)

Debug metrics include:
- Creation/destruction signals with 🟢/🔴 indicators
- Real-time data updates with 📡/📤 send/receive tracking
- Window sizing info with 📐 dimensions and font calculations
- User interaction feedback with 🖱️ tap/long-press detection

This implementation provides reliable floating window functionality
with proper data synchronization and efficient resource usage.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 23:36:16 +09:00
Mikkeli Matlock 9cd497f4f5 Merge master branch improvements into dev/floating-window-wip
Resolved conflicts:
- CLAUDE.md: Updated documentation to match master branch structure
- main.dart: Hybrid overlay sizing - proportional with reasonable limits (280-400w, 140-300h)

This brings the layout improvements from master (better flex ratios and sizing)
into the advanced floating window development branch.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 19:33:56 +09:00
Mikkeli Matlock 057f33fc50 Improve floating window sizing and layout ratios
- Increase overlay window size from 180x100 to 320x280 for better visibility
- Adjust portrait layout flex ratios: speed area 65, compass area 32
- Better proportions for overlay and main app interface

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 19:31:33 +09:00
Mikkeli Matlock 550755fad9 Update project documentation for v2.1.0 and dev branch strategy
- Updated TODO.md and CLAUDE.md to reflect current branch structure
- Master branch (v2.1.0): stable release with basic floating window
- Dev branch: advanced floating window marked as seriously WIP
- Clear documentation of branch strategy and known issues

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 19:31:21 +09:00
Mikkeli Matlock 940aaee135 Complete floating window WIP documentation updates 2025-08-25 23:41:27 +09:00
Mikkeli Matlock 6c9bada84d Add floating window overlay implementation (seriously WIP)
DEVELOPMENT BRANCH - NOT READY FOR PRODUCTION

Floating window overlay functionality:
- Basic GPS data synchronization implemented but unreliable
- Theme/unit sync between main app and overlay works partially
- Proportional sizing and positioning needs major refinement
- Close button works but overlay behavior inconsistent
- Android SYSTEM_ALERT_WINDOW permissions configured

Known issues:
- Overlay reliability inconsistent across devices/Android versions
- GPS sync unreliable, performance impact possible
- Positioning and sizing calculations need major work

This is a work-in-progress development branch. Main functionality
works but is seriously buggy and not ready for release.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-25 23:40:58 +09:00
6 changed files with 661 additions and 151 deletions
+2 -1
View File
@@ -15,7 +15,8 @@
"Bash(powershell:*)",
"WebFetch(domain:docs.anthropic.com)",
"WebSearch",
"WebFetch(domain:pub.dev)"
"WebFetch(domain:pub.dev)",
"Bash(git commit:*)"
],
"defaultMode": "acceptEdits"
},
+33 -11
View File
@@ -25,21 +25,29 @@ A customizable Flutter GPS speedometer app targeting Android devices, emphasizin
### Technical Stack
```
Dependencies: geolocator, permission_handler, provider, shared_preferences, flutter_material_color_picker
Dependencies: geolocator, permission_handler, provider, shared_preferences, flutter_material_color_picker, flutter_overlay_window
Platforms: Android (primary), iOS support included but untested
Architecture: Provider + Consumer pattern for reactive state management
Permissions: Location services with graceful degradation
Permissions: Location services + SYSTEM_ALERT_WINDOW for overlay functionality
```
## Recent Updates (v2.1.0)
### ✅ Completed Features
- [x] **Floating Window Overlay**: System-level overlay window for persistent speed display
- [x] **Split Decimal Display**: Main speed prominent, decimal fraction smaller and faded
- [x] **Optimized Content Sizing**: Maximized readability with improved flex ratios
- [x] **Navigation Icon Trigger**: Tap compass area to launch floating window
- [x] **Multiple Close Options**: Overlay close button and long-press navigation area
- [x] **Android Permissions**: SYSTEM_ALERT_WINDOW and foreground service support
### ✅ Completed Features (Master Branch - Stable)
- [x] **Split Decimal Display**: Main speed prominent with smaller, faded decimal fraction for better readability
- [x] **Optimized Content Sizing**: Maximized readability with improved responsive layout and flex ratios
- [x] **Basic Floating Window**: Simple overlay with "GPS SPEEDO" text and close button (stable implementation)
- [x] **Navigation Icon Trigger**: Tap compass/navigation area to launch floating window overlay
- [x] **Multiple Close Options**: Overlay close button and long-press navigation area for easy dismissal
- [x] **Android System Integration**: SYSTEM_ALERT_WINDOW permissions and foreground service configuration
### 🚧 Work in Progress Features (dev/floating-window-wip Branch - Seriously Buggy)
- [ ] **Advanced Floating Window Overlay**: System-level overlay with live GPS data - basic functionality works but seriously buggy
- GPS data synchronization implemented but unreliable
- Theme/unit sync between main app and overlay works partially
- Proportional sizing and positioning needs major refinement
- Close button functionality works but overlay behavior inconsistent
- Performance may impact main app, needs optimization
### Previous Updates (v1.1)
- [x] **Portrait/Landscape Support**: Automatic orientation detection implemented
@@ -48,10 +56,19 @@ Permissions: Location services with graceful degradation
## TODO - Future Improvements
### Critical Issues
- [ ] **Dependency Updates**: Update 10 packages to latest compatible versions
### Known Issues
**Master Branch (v2.1.0 - Stable):**
- [ ] **Dependency Updates**: Update packages to latest compatible versions
- [ ] **Orientation Override**: Fix auto-rotate to work regardless of system toggle status
**Dev Branch (floating-window-wip - Seriously Buggy):**
- [ ] **Floating Window Reliability**: Overlay behavior inconsistent across different devices and Android versions
- [ ] **GPS Data Sync**: Real-time GPS synchronization between main app and overlay unreliable
- [ ] **Theme/Unit Sync**: Setting synchronization works partially but not consistently
- [ ] **Overlay Positioning**: Poor positioning stability, sizing calculations need major work
- [ ] **Performance**: Overlay may impact main app performance, needs optimization
### UI/UX Enhancements
- [ ] **Background Customization**: Custom background color selection
- [ ] **Interface Rearrangement**: User-defined layout customization
@@ -74,6 +91,11 @@ Permissions: Location services with graceful degradation
- **Memory**: Project uses automatic Claude Code memory - this file for major decisions only
- **Git**: Active repository at `https://github.com/mikkelimatlock/gps_speedo`
## Branch Strategy
- **master**: Stable releases, ready for production APK builds
- **dev/floating-window-wip**: Advanced floating window development (seriously buggy, not for release)
- Tagged releases: v2.1.0 on master branch contains stable basic floating window
## Code Patterns to Maintain
- Provider pattern for state management
- Separate widgets for reusability
+11 -2
View File
@@ -10,10 +10,17 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<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
android:label="Speedo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:allowBackup="false"
android:killAfterRestore="false"
android:largeHeap="true">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -22,7 +29,9 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
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
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
+603 -136
View File
@@ -1,14 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart'; // For kDebugMode
import 'package:flutter/services.dart'; // For HapticFeedback
import 'dart:async';
import 'package:geolocator/geolocator.dart';
import 'package:wakelock_plus/wakelock_plus.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 'gps_service.dart';
import 'color_themes.dart';
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() {
runApp(const SpeedoApp());
}
@@ -16,57 +28,10 @@ void main() {
// Entry point for overlay window
@pragma("vm:entry-point")
void overlayMain() {
runApp(MaterialApp(
customDebugPrint('[Overlay] 🚀 overlayMain() called');
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.transparent,
body: Container(
width: 180,
height: 100,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.withOpacity(0.7), width: 2),
),
child: Stack(
children: [
const Center(
child: Text(
'GPS\nSPEEDO',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.green,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
// Close button
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => FlutterOverlayWindow.closeOverlay(),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.8),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white, width: 1),
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 16,
),
),
),
),
],
),
),
),
home: OverlaySpeedometer(),
));
}
@@ -102,11 +67,39 @@ class SpeedometerScreen extends StatefulWidget {
class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindingObserver {
double _speed = 0.0;
double _heading = -1.0;
double _realSpeed = 0.0; // Store authentic GPS values
double _realHeading = -1.0;
SpeedUnit _currentUnit = SpeedUnit.kmh;
int _currentThemeIndex = 0;
StreamSubscription<Position>? _positionSubscription;
StreamSubscription<dynamic>? _overlaySubscription;
String _errorMessage = '';
bool _isInBackground = 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
void initState() {
@@ -114,12 +107,112 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
WidgetsBinding.instance.addObserver(this);
_initializeGps();
_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 {
try {
// Force enable wake lock even if already enabled for robustness
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) {
customDebugPrint('[Main] ❌ Wake lock failed: $e');
// Wake lock not supported on this platform, continue normally
}
}
@@ -128,7 +221,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_positionSubscription?.cancel();
_overlaySubscription?.cancel();
_backgroundHeartbeatTimer?.cancel();
_overlayDataPushTimer?.cancel();
WakelockPlus.disable();
customDebugPrint('[Main] 🛑 Main app disposed - all subscriptions and timers canceled');
super.dispose();
}
@@ -156,16 +253,57 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
}
void _handleBackgroundTransition() {
// When app goes to background, show floating window if GPS is active and no error
print('App went to background, checking conditions for floating window...');
print('Speed: $_speed, Error: $_errorMessage');
// Temporarily disable auto-trigger for debugging
// if (_speed >= 0 && _errorMessage.isEmpty) {
// _showFloatingWindow();
// }
// Enhanced background persistence
customDebugPrint('[Main] 🌙 App going to background - activating persistence measures');
// 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 {
// Always initialize real GPS for authentic behavior
try {
final serviceEnabled = await GpsService.isLocationServiceEnabled();
if (!serviceEnabled) {
@@ -189,60 +327,191 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
}
},
);
customDebugPrint('[Main] 📡 Real GPS initialized');
} catch (e) {
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) {
setState(() {
_speed = position.speed;
_heading = position.heading;
// Always store real GPS values
_realSpeed = position.speed;
_realHeading = position.heading;
// Use real values for display (debug mode will override these)
_speed = _realSpeed;
_heading = _realHeading;
_errorMessage = '';
});
customDebugPrint('[Main] 📡 Real GPS: ${_realSpeed.toStringAsFixed(1)} m/s, ${_realHeading.toStringAsFixed(1)}°');
// 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() {
// 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
final displaySpeed = _currentUnit.convert(_speed);
final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString();
final headingText = GpsService.formatHeading(_heading);
customDebugPrint('[Main] 📤 SENDING to overlay:');
customDebugPrint(' speedText: "$speedText"');
customDebugPrint(' headingText: "$headingText"');
customDebugPrint(' direction: ${_heading.toStringAsFixed(1)}°');
customDebugPrint(' unit: ${_currentUnit.label}, theme: $_currentThemeIndex');
FlutterOverlayWindow.shareData({
'action': 'updateDisplay',
'speedText': speedText,
'headingText': headingText,
'heading': _heading,
'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex,
});
}
void _cycleUnit() {
setState(() {
_currentUnit = _currentUnit.next;
});
// Push updated display data to overlay
_pushDataToOverlay();
}
void _cycleTheme() {
setState(() {
_currentThemeIndex = ColorThemes.getNextThemeIndex(_currentThemeIndex);
});
// Push updated display data to overlay
_pushDataToOverlay();
}
Future<void> _showFloatingWindow() async {
try {
// Share current unit with overlay
// Use centralized sizing function
final overlaySize = _getOverlaySize();
// Send initial display data WITH size info (only needed on creation)
final displaySpeed = _currentUnit.convert(_speed);
final speedText = displaySpeed < 1.0 ? '--' : displaySpeed.toInt().toString();
final headingText = GpsService.formatHeading(_heading);
await FlutterOverlayWindow.shareData({
'action': 'updateUnit',
'action': 'updateDisplay',
'speedText': speedText,
'headingText': headingText,
'heading': _heading,
'unitIndex': _currentUnit.index,
'themeIndex': _currentThemeIndex,
'overlayWidth': overlaySize['width']!.toDouble(),
'overlayHeight': overlaySize['height']!.toDouble(),
});
// Show the overlay with safer positioning
// Show the overlay with proportional sizing based on system resolution
await FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "GPS Speedometer",
overlayContent: 'GPS Speedometer Overlay',
overlayTitle: "Speedometer",
overlayContent: 'Speedo overlay active',
flag: OverlayFlag.defaultFlag,
visibility: NotificationVisibility.visibilityPublic,
positionGravity: PositionGravity.none,
width: 180,
height: 100,
// Use consistent sizing from centralized function
width: overlaySize['width']!,
height: overlaySize['height']!,
);
customDebugPrint('[Main] 🟢 OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}');
// 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) {
print('Error showing floating window: $e');
// Silent error handling - floating window issues shouldn't crash main app
}
}
Future<void> _closeFloatingWindow() async {
if (!_isOverlayActive) {
customDebugPrint('[Main] ⚠️ No overlay to close');
return;
}
try {
await FlutterOverlayWindow.closeOverlay();
customDebugPrint('[Main] 🔴 OVERLAY CLOSED - Requested from main app');
_handleOverlayClose(); // This will set _isOverlayActive = false and stop listener
} catch (e) {
print('Error closing floating window: $e');
customDebugPrint('[Main] ❌ Error closing overlay: $e');
}
}
@@ -327,7 +596,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
children: [
// Speed area - takes most space, precise tap targets on text only
Expanded(
flex: 7,
flex: 65,
child: Container(
width: double.infinity,
color: Colors.transparent,
@@ -349,7 +618,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
)
else ...[
Flexible(
flex: 4,
flex: 7,
child: GestureDetector(
onTap: _cycleTheme, // Tap speed to cycle theme
child: FittedBox(
@@ -360,6 +629,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
Flexible(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
Flexible(
flex: 2,
child: GestureDetector(
onTap: _cycleUnit, // clicking on unit to cycle unit is more intuitive
child: FittedBox(
@@ -375,6 +650,12 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
),
),
Flexible(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
],
],
),
@@ -382,10 +663,20 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
// Compass area - compact but fully tappable for floating window
Expanded(
flex: 3,
flex: 32,
child: GestureDetector(
onTap: _showFloatingWindow, // Tap navigation icon to show floating window
onLongPress: _closeFloatingWindow, // Long press to close floating window
onTap: () {
customDebugPrint('[Main] 🖱️ Navigation area tapped - overlay active: $_isOverlayActive');
if (_isOverlayActive) {
_closeFloatingWindow();
} else {
_showFloatingWindow();
}
},
onLongPress: () {
customDebugPrint('[Main] 🖱️ Navigation area long-pressed - overlay active: $_isOverlayActive');
_closeFloatingWindow();
},
child: Container(
width: double.infinity,
color: Colors.transparent,
@@ -435,7 +726,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
children: [
// Speed area - takes majority of space, precise tap targets on text only
Expanded(
flex: 66,
flex: 60,
child: Container(
height: double.infinity,
color: Colors.transparent,
@@ -457,7 +748,11 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
)
else ...[
Flexible(
flex: 4,
flex: 1,
child: FittedBox(),
),
Flexible(
flex: 20,
child: GestureDetector(
onTap: _cycleTheme,
child: FittedBox(
@@ -467,7 +762,7 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
),
Flexible(
flex: 1,
flex: 7,
child: GestureDetector(
onTap: _cycleUnit,
child: FittedBox(
@@ -483,6 +778,10 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
),
),
Flexible(
flex: 5,
child: FittedBox(),
),
],
],
),
@@ -490,10 +789,20 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
// Compass area - smaller but fully tappable for floating window
Expanded(
flex: 33,
flex: 35,
child: GestureDetector(
onTap: _showFloatingWindow,
onLongPress: _closeFloatingWindow, // Long press to close floating window
onTap: () {
customDebugPrint('[Main] 🖱️ Navigation area tapped (landscape) - overlay active: $_isOverlayActive');
if (_isOverlayActive) {
_closeFloatingWindow();
} else {
_showFloatingWindow();
}
},
onLongPress: () {
customDebugPrint('[Main] 🖱️ Navigation area long-pressed (landscape) - overlay active: $_isOverlayActive');
_closeFloatingWindow();
},
child: Container(
height: double.infinity,
color: Colors.transparent,
@@ -534,82 +843,240 @@ class _SpeedometerScreenState extends State<SpeedometerScreen> with WidgetsBindi
),
),
),
Expanded(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
),
),
],
);
}
}
class SimpleOverlaySpeedometer extends StatelessWidget {
const SimpleOverlaySpeedometer({super.key});
class OverlaySpeedometer extends StatefulWidget {
const OverlaySpeedometer({super.key});
@override
State<OverlaySpeedometer> createState() => _OverlaySpeedometerState();
}
class _OverlaySpeedometerState extends State<OverlaySpeedometer> {
String _speedText = '--';
String _headingText = 'N/A';
double _heading = -1.0;
int _currentThemeIndex = 0;
double _systemScreenWidth = 400;
double _overlayWidth = 280;
double _overlayHeight = 140;
@override
void initState() {
super.initState();
customDebugPrint('[Overlay] 🟢 CREATED - Floating window initialized');
_getSystemScreenSize();
_listenToMainAppMessages();
}
void _getSystemScreenSize() {
try {
// Get actual system screen resolution, not just app window size
final window = WidgetsBinding.instance.platformDispatcher.views.first;
final physicalSize = window.physicalSize;
final devicePixelRatio = window.devicePixelRatio;
final systemSize = physicalSize / devicePixelRatio;
customDebugPrint('[Overlay] 🔍 Screen detection:');
customDebugPrint(' physicalSize: ${physicalSize.width.round()}x${physicalSize.height.round()}');
customDebugPrint(' devicePixelRatio: $devicePixelRatio');
customDebugPrint(' calculated systemSize: ${systemSize.width.round()}x${systemSize.height.round()}');
setState(() {
_systemScreenWidth = systemSize.width;
});
customDebugPrint('[Overlay] ✅ _systemScreenWidth set to: ${_systemScreenWidth.round()}');
} catch (e) {
customDebugPrint('[Overlay] ❌ Error getting system screen size: $e');
// Fallback to reasonable default
setState(() {
_systemScreenWidth = 400;
});
}
}
void _listenToMainAppMessages() {
FlutterOverlayWindow.overlayListener.listen((data) {
if (data is Map && data['action'] == 'updateDisplay') {
setState(() {
_speedText = data['speedText'] ?? '--';
_headingText = data['headingText'] ?? 'N/A';
_heading = data['heading']?.toDouble() ?? -1.0;
_currentThemeIndex = data['themeIndex'] ?? 0;
// Update screen size and overlay dimensions if provided by main app
if (data.containsKey('systemScreenWidth')) {
_systemScreenWidth = data['systemScreenWidth']?.toDouble() ?? _systemScreenWidth;
}
if (data.containsKey('overlayWidth')) {
_overlayWidth = data['overlayWidth']?.toDouble() ?? _overlayWidth;
}
if (data.containsKey('overlayHeight')) {
_overlayHeight = data['overlayHeight']?.toDouble() ?? _overlayHeight;
}
});
customDebugPrint('[Overlay] 📡 UPDATE received:');
customDebugPrint(' speedText: "$_speedText"');
customDebugPrint(' headingText: "$_headingText"');
customDebugPrint(' direction: ${_heading.toStringAsFixed(1)}°');
customDebugPrint(' themeIndex: $_currentThemeIndex');
customDebugPrint(' systemScreenWidth: ${_systemScreenWidth.round()}');
customDebugPrint(' overlaySize from main: ${_overlayWidth.round()}x${_overlayHeight.round()}');
}
});
}
@override
void dispose() {
customDebugPrint('[Overlay] 🔴 DESTROYED - Floating window disposed');
// No GPS subscription to cancel - overlay is display-only
super.dispose();
}
@override
Widget build(BuildContext context) {
print("SimpleOverlaySpeedometer build() called");
final currentTheme = ColorThemes.getTheme(_currentThemeIndex);
// Use overlay dimensions provided by main app (not calculated locally)
final fontSize = (_overlayWidth * 0.2); // Font size proportional to actual overlay width
customDebugPrint('[Overlay] 📐 Window size: ${_overlayWidth.round()}x${_overlayHeight.round()}, fontSize: ${fontSize.round()}');
return Material(
color: Colors.transparent,
child: Container(
width: 200,
height: 120,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.9), // Highly visible red background
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.yellow, width: 3), // Bright yellow border
),
child: Stack(
children: [
// Test content - much more visible
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: SizedBox(
// Use SizedBox to fill the overlay window completely
width: double.infinity,
height: double.infinity,
child: Container(
decoration: BoxDecoration(
color: currentTheme.background.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: currentTheme.speedText.withValues(alpha: 0.3),
width: 1
),
),
child: Stack(
children: [
// Main content - landscape layout (1:1 flex as per NOTES.txt)
Row(
children: [
const Text(
'FLOATING',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
// Speed section - left half
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.all(8),
child: Center(
child: FittedBox(
fit: BoxFit.contain,
child: Text(
(_speedText == "--" ? "0" : _speedText),
style: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.w300,
color: currentTheme.speedText,
fontFamily: 'DIN1451Alt',
),
),
),
),
),
),
const Text(
'WINDOW',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.yellow,
),
),
const Text(
'WORKING!',
style: TextStyle(
fontSize: 12,
color: Colors.white,
// Direction section - right half (same as full-screen behavior)
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
flex: 2,
child: Transform.rotate(
angle: (_heading >= 0 && _heading < 360) ? (_heading * math.pi / 180.0) : 0,
child: FittedBox(
fit: BoxFit.contain,
child: Icon(
Icons.navigation,
size: fontSize * 0.8,
color: currentTheme.headingText,
),
),
),
),
Flexible(
flex: 1,
child: FittedBox(
fit: BoxFit.contain,
child: Text(
_headingText,
style: TextStyle(
fontSize: fontSize * 0.4,
color: currentTheme.headingText,
fontFamily: 'DIN1451Alt',
),
),
),
),
],
),
),
),
],
),
),
// Close button
Positioned(
top: 2,
right: 2,
child: GestureDetector(
onTap: () => FlutterOverlayWindow.closeOverlay(),
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Color.fromARGB(255, 255, 171, 16), // call the current background colour defined as in `color_themes.dart`
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 12,
// Transparent full-overlay gesture detection layer
Positioned.fill(
child: GestureDetector(
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(
color: Colors.transparent,
),
),
),
),
],
],
),
),
),
);
+8
View File
@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
+4 -1
View File
@@ -2,7 +2,7 @@ name: gps_speedo
description: "Speedo - A Flutter application."
publish_to: 'none'
version: 2.1.0+2
version: 2.2.0+1
environment:
sdk: ^3.9.0
@@ -23,6 +23,9 @@ dependencies:
# Floating window overlay capability
flutter_overlay_window: ^0.5.0
# Bring app to foreground from background/overlay
bg_launcher: ^0.1.0
dev_dependencies: