diff options
Diffstat (limited to 'android-app/app/src/main')
10 files changed, 384 insertions, 222 deletions
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml index 2fce535..7c2c02d 100644 --- a/android-app/app/src/main/AndroidManifest.xml +++ b/android-app/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.INTERNET" /> <application @@ -11,6 +13,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.AndroidApp"> + <service + android:name=".LocationService" + android:foregroundServiceType="location" /> <activity android:name=".MainActivity" android:exported="true"> diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/LocationService.kt b/android-app/app/src/main/kotlin/com/example/androidapp/LocationService.kt deleted file mode 100644 index ca73397..0000000 --- a/android-app/app/src/main/kotlin/com/example/androidapp/LocationService.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.example.androidapp - -import android.annotation.SuppressLint -import android.content.Context -import android.location.Location -import android.os.Looper -import com.google.android.gms.location.* -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.update -import android.util.Log // Import Log for logging -import kotlinx.coroutines.tasks.await // Import await for Task conversion - -data class GpsData( - val latitude: Double, - val longitude: Double, - val speedOverGround: Float, // m/s - val courseOverGround: Float // degrees -) { - fun toLocation(): Location { - val location = Location("GpsData") - location.latitude = latitude - location.longitude = longitude - location.speed = speedOverGround - location.bearing = courseOverGround - return location - } -} - -class LocationService(private val context: Context) { - - private val fusedLocationClient: FusedLocationProviderClient = - LocationServices.getFusedLocationProviderClient(context) - - // StateFlow to hold the current anchor watch state - private val _anchorWatchState = MutableStateFlow(AnchorWatchState()) - val anchorWatchState: StateFlow<AnchorWatchState> = _anchorWatchState - - // Anchor alarm manager - private val anchorAlarmManager = AnchorAlarmManager(context) - private var isAlarmTriggered = false // To prevent repeated alarm triggering - - @SuppressLint("MissingPermission") // Permissions handled by the calling component (Activity/Fragment) - fun getLocationUpdates(): Flow<GpsData> = callbackFlow { - val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) - .setMinUpdateIntervalMillis(500) - .build() - - val locationCallback = object : LocationCallback() { - override fun onLocationResult(locationResult: LocationResult) { - locationResult.lastLocation?.let { location -> - val gpsData = GpsData( - latitude = location.latitude, - longitude = location.longitude, - speedOverGround = location.speed, - courseOverGround = location.bearing - ) - trySend(gpsData) - - // Check for anchor drag if anchor watch is active - _anchorWatchState.update { currentState -> - if (currentState.isActive && currentState.anchorLocation != null) { - val isDragging = currentState.isDragging(location) - if (isDragging) { - Log.w("AnchorWatch", "!!! ANCHOR DRAG DETECTED !!! Distance: ${currentState.anchorLocation.distanceTo(location)}m, Radius: ${currentState.watchCircleRadiusMeters}m") - if (!isAlarmTriggered) { - anchorAlarmManager.startAlarm() - isAlarmTriggered = true - } - } else { - Log.d("AnchorWatch", "Anchor holding. Distance: ${currentState.anchorLocation.distanceTo(location)}m, Radius: ${currentState.watchCircleRadiusMeters}m") - if (isAlarmTriggered) { - anchorAlarmManager.stopAlarm() - isAlarmTriggered = false - } - } - } else { - // If anchor watch is not active, ensure alarm is stopped - if (isAlarmTriggered) { - anchorAlarmManager.stopAlarm() - isAlarmTriggered = false - } - } - currentState // Return the current state (no change unless we explicitly want to update something here) - } - } - } - } - - fusedLocationClient.requestLocationUpdates( - locationRequest, - locationCallback, - Looper.getMainLooper() - ) - - awaitClose { - fusedLocationClient.removeLocationUpdates(locationCallback) - } - } - - /** - * Starts the anchor watch with the current location as the anchor point. - * @param radiusMeters The watch circle radius in meters. - */ - @SuppressLint("MissingPermission") - suspend fun startAnchorWatch(radiusMeters: Double = AnchorWatchState.DEFAULT_WATCH_CIRCLE_RADIUS_METERS) { - val lastLocation = fusedLocationClient.lastLocation.await() // Using await() from kotlinx.coroutines.tasks - lastLocation?.let { location -> - _anchorWatchState.value = AnchorWatchState( - anchorLocation = location, - watchCircleRadiusMeters = radiusMeters, - setTimeMillis = System.currentTimeMillis(), - isActive = true - ) - Log.i("AnchorWatch", "Anchor watch started at lat: ${location.latitude}, lon: ${location.longitude} with radius: ${radiusMeters}m") - } ?: run { - Log.e("AnchorWatch", "Could not start anchor watch: Last known location is null.") - // Handle error, e.g., show a toast to the user - } - } - - /** - * Stops the anchor watch. - */ - fun stopAnchorWatch() { - _anchorWatchState.value = AnchorWatchState(isActive = false) - Log.i("AnchorWatch", "Anchor watch stopped.") - anchorAlarmManager.stopAlarm() // Ensure alarm is stopped when anchor watch is explicitly stopped - isAlarmTriggered = false - } - - /** - * Updates the watch circle radius. - */ - fun updateWatchCircleRadius(radiusMeters: Double) { - _anchorWatchState.update { it.copy(watchCircleRadiusMeters = radiusMeters) } - Log.d("AnchorWatch", "Watch circle radius updated to ${radiusMeters}m.") - } -} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorAlarmManager.kt b/android-app/app/src/main/kotlin/org/terst/nav/AnchorAlarmManager.kt index 4b31719..d4423db 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorAlarmManager.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/AnchorAlarmManager.kt @@ -1,4 +1,4 @@ -package com.example.androidapp +package org.terst.nav import android.app.NotificationChannel import android.app.NotificationManager diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorWatchData.kt b/android-app/app/src/main/kotlin/org/terst/nav/AnchorWatchData.kt index c7c13fd..03e6a2f 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorWatchData.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/AnchorWatchData.kt @@ -1,4 +1,4 @@ -package com.example.androidapp +package org.terst.nav import android.location.Location diff --git a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt new file mode 100644 index 0000000..4b59139 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt @@ -0,0 +1,254 @@ +package org.terst.nav + +import android.annotation.SuppressLint +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.location.Location +import android.os.IBinder +import android.os.Looper +import androidx.core.app.NotificationCompat +import com.google.android.gms.location.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import android.util.Log +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +data class GpsData( + val latitude: Double, + val longitude: Double, + val speedOverGround: Float, // m/s + val courseOverGround: Float // degrees +) { + fun toLocation(): Location { + val location = Location("GpsData") + location.latitude = latitude + location.longitude = longitude + location.speed = speedOverGround + location.bearing = courseOverGround + return location + } +} + +class LocationService : Service() { + + private lateinit var fusedLocationClient: FusedLocationProviderClient + private lateinit var locationCallback: LocationCallback + private lateinit var anchorAlarmManager: AnchorAlarmManager + private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val NOTIFICATION_CHANNEL_ID = "location_service_channel" + private val NOTIFICATION_ID = 123 + + private var isAlarmTriggered = false // To prevent repeated alarm triggering + + override fun onCreate() { + super.onCreate() + Log.d("LocationService", "Service created") + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + anchorAlarmManager = AnchorAlarmManager(this) // Initialize with service context + createNotificationChannel() + + locationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + locationResult.lastLocation?.let { location -> + val gpsData = GpsData( + latitude = location.latitude, + longitude = location.longitude, + speedOverGround = location.speed, + courseOverGround = location.bearing + ) + serviceScope.launch { + _locationFlow.emit(gpsData) // Emit to shared flow + } + + + // Check for anchor drag if anchor watch is active + _anchorWatchState.update { currentState -> + if (currentState.isActive && currentState.anchorLocation != null) { + val isDragging = currentState.isDragging(location) + if (isDragging) { + Log.w("AnchorWatch", "!!! ANCHOR DRAG DETECTED !!! Distance: ${currentState.anchorLocation.distanceTo(location)}m, Radius: ${currentState.watchCircleRadiusMeters}m") + if (!isAlarmTriggered) { + anchorAlarmManager.startAlarm() + isAlarmTriggered = true + } + } else { + Log.d("AnchorWatch", "Anchor holding. Distance: ${currentState.anchorLocation.distanceTo(location)}m, Radius: ${currentState.watchCircleRadiusMeters}m") + if (isAlarmTriggered) { + anchorAlarmManager.stopAlarm() + isAlarmTriggered = false + } + } + } else { + // If anchor watch is not active, ensure alarm is stopped + if (isAlarmTriggered) { + anchorAlarmManager.stopAlarm() + isAlarmTriggered = false + } + } + currentState + } + } + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_START_FOREGROUND_SERVICE -> { + Log.d("LocationService", "Starting foreground service") + startForeground(NOTIFICATION_ID, createNotification()) + startLocationUpdatesInternal() + } + ACTION_STOP_FOREGROUND_SERVICE -> { + Log.d("LocationService", "Stopping foreground service") + stopLocationUpdatesInternal() + stopSelf() + } + ACTION_START_ANCHOR_WATCH -> { + Log.d("LocationService", "Received ACTION_START_ANCHOR_WATCH") + val radius = intent.getDoubleExtra(EXTRA_WATCH_RADIUS, AnchorWatchState.DEFAULT_WATCH_CIRCLE_RADIUS_METERS) + serviceScope.launch { startAnchorWatch(radius) } + } + ACTION_STOP_ANCHOR_WATCH -> { + Log.d("LocationService", "Received ACTION_STOP_ANCHOR_WATCH") + stopAnchorWatch() + } + ACTION_UPDATE_WATCH_RADIUS -> { + Log.d("LocationService", "Received ACTION_UPDATE_WATCH_RADIUS") + val radius = intent.getDoubleExtra(EXTRA_WATCH_RADIUS, AnchorWatchState.DEFAULT_WATCH_CIRCLE_RADIUS_METERS) + updateWatchCircleRadius(radius) + } + } + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? { + return null // Not a bound service + } + + override fun onDestroy() { + super.onDestroy() + Log.d("LocationService", "Service destroyed") + stopLocationUpdatesInternal() + anchorAlarmManager.stopAlarm() + _anchorWatchState.value = AnchorWatchState(isActive = false) + isAlarmTriggered = false // Reset alarm trigger state + serviceScope.cancel() // Cancel the coroutine scope + } + + @SuppressLint("MissingPermission") + private fun startLocationUpdatesInternal() { + Log.d("LocationService", "Requesting location updates") + val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) + .setMinUpdateIntervalMillis(500) + .build() + fusedLocationClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + } + + private fun stopLocationUpdatesInternal() { + Log.d("LocationService", "Removing location updates") + fusedLocationClient.removeLocationUpdates(locationCallback) + } + + private fun createNotificationChannel() { + val serviceChannel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + "Location Service Channel", + NotificationManager.IMPORTANCE_LOW + ) + val manager = getSystemService(NotificationManager::class.java) as NotificationManager + manager.createNotificationChannel(serviceChannel) + } + + private fun createNotification(): Notification { + val notificationIntent = Intent(this, MainActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setContentTitle("Sailing Companion") + .setContentText("Tracking your location in the background...") + .setSmallIcon(R.drawable.ic_anchor) + .setContentIntent(pendingIntent) + .build() + } + + /** + * Starts the anchor watch with the current location as the anchor point. + * @param radiusMeters The watch circle radius in meters. + */ + @SuppressLint("MissingPermission") + suspend fun startAnchorWatch(radiusMeters: Double = AnchorWatchState.DEFAULT_WATCH_CIRCLE_RADIUS_METERS) { + val lastLocation = fusedLocationClient.lastLocation.await() + lastLocation?.let { location -> + _anchorWatchState.update { AnchorWatchState( + anchorLocation = location, + watchCircleRadiusMeters = radiusMeters, + setTimeMillis = System.currentTimeMillis(), + isActive = true + ) } + Log.i("AnchorWatch", "Anchor watch started at lat: ${location.latitude}, lon: ${location.longitude} with radius: ${radiusMeters}m") + } ?: run { + Log.e("AnchorWatch", "Could not start anchor watch: Last known location is null.") + // Handle error, e.g., show a toast to the user + } + } + + /** + * Stops the anchor watch. + */ + fun stopAnchorWatch() { + _anchorWatchState.update { AnchorWatchState(isActive = false) } + Log.i("AnchorWatch", "Anchor watch stopped.") + anchorAlarmManager.stopAlarm() + isAlarmTriggered = false + } + + /** + * Updates the watch circle radius. + */ + fun updateWatchCircleRadius(radiusMeters: Double) { + _anchorWatchState.update { it.copy(watchCircleRadiusMeters = radiusMeters) } + Log.d("AnchorWatch", "Watch circle radius updated to ${radiusMeters}m.") + } + + companion object { + const val ACTION_START_FOREGROUND_SERVICE = "ACTION_START_FOREGROUND_SERVICE" + const val ACTION_STOP_FOREGROUND_SERVICE = "ACTION_STOP_FOREGROUND_SERVICE" + const val ACTION_START_ANCHOR_WATCH = "ACTION_START_ANCHOR_WATCH" + const val ACTION_STOP_ANCHOR_WATCH = "ACTION_STOP_ANCHOR_WATCH" + const val ACTION_UPDATE_WATCH_RADIUS = "ACTION_UPDATE_WATCH_RADIUS" + const val EXTRA_WATCH_RADIUS = "extra_watch_radius" + + // Publicly accessible flows + val locationFlow: SharedFlow<GpsData> + get() = _locationFlow + val anchorWatchState: StateFlow<AnchorWatchState> + get() = _anchorWatchState + + private val _locationFlow = MutableSharedFlow<GpsData>(replay = 1) + private val _anchorWatchState = MutableStateFlow(AnchorWatchState()) + } +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index f1f8c4d..ccdf32f 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -1,11 +1,13 @@ -package com.example.androidapp +package org.terst.nav import android.Manifest import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.location.Location import android.media.MediaPlayer +import android.os.Build import android.os.Bundle +import android.content.Intent import android.util.Log import android.view.View import android.widget.Button @@ -35,11 +37,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale import java.util.concurrent.TimeUnit -import kotlinx.coroutines.tasks.await +//import kotlinx.coroutines.tasks.await // Removed as we're no longer directly accessing FusedLocationProviderClient import kotlin.math.cos import kotlin.math.sin import kotlin.math.sqrt @@ -79,7 +82,8 @@ class MainActivity : AppCompatActivity() { private lateinit var mobValueElapsedTime: TextView private lateinit var mobRecoveredButton: Button - private lateinit var locationService: LocationService + // Removed direct locationService instance + // private lateinit var locationService: LocationService // MOB State private var mobActivated: Boolean = false @@ -116,11 +120,16 @@ class MainActivity : AppCompatActivity() { // system permissions dialog. private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true && - permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true) { - // Permissions granted, initialize location service and start updates + val fineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true + val coarseLocationGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true + val backgroundLocationGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + permissions[Manifest.permission.ACCESS_BACKGROUND_LOCATION] == true + } else true // Not needed below Android 10 + + if (fineLocationGranted && coarseLocationGranted && backgroundLocationGranted) { + // Permissions granted, start location service and observe updates Toast.makeText(this, "Location permissions granted", Toast.LENGTH_SHORT).show() - locationService = LocationService(this) + startLocationService() observeLocationUpdates() // Start observing location updates observeAnchorWatchState() // Start observing anchor watch state } else { @@ -136,16 +145,24 @@ class MainActivity : AppCompatActivity() { MapLibre.getInstance(this) setContentView(R.layout.activity_main) + val permissionsToRequest = mutableListOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + permissionsToRequest.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) + } + // Check and request location permissions - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - requestPermissionLauncher.launch(arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.PERMISSION_ACCESS_COARSE_LOCATION - )) + val allPermissionsGranted = permissionsToRequest.all { + ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED + } + + if (!allPermissionsGranted) { + requestPermissionLauncher.launch(permissionsToRequest.toTypedArray()) } else { - // Permissions already granted, initialize location service - locationService = LocationService(this) + // Permissions already granted, start location service + startLocationService() observeLocationUpdates() // Start observing location updates observeAnchorWatchState() // Start observing anchor watch state } @@ -267,35 +284,38 @@ class MainActivity : AppCompatActivity() { buttonDecreaseRadius.setOnClickListener { currentWatchCircleRadius = (currentWatchCircleRadius - 5).coerceAtLeast(10.0) // Minimum 10m anchorRadiusText.text = String.format(Locale.getDefault(), "Radius: %.1fm", currentWatchCircleRadius) - if (::locationService.isInitialized) { - locationService.updateWatchCircleRadius(currentWatchCircleRadius) + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_UPDATE_WATCH_RADIUS + putExtra(LocationService.EXTRA_WATCH_RADIUS, currentWatchCircleRadius) } + startService(intent) } buttonIncreaseRadius.setOnClickListener { currentWatchCircleRadius = (currentWatchCircleRadius + 5).coerceAtMost(200.0) // Maximum 200m anchorRadiusText.text = String.format(Locale.getDefault(), "Radius: %.1fm", currentWatchCircleRadius) - if (::locationService.isInitialized) { - locationService.updateWatchCircleRadius(currentWatchCircleRadius) + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_UPDATE_WATCH_RADIUS + putExtra(LocationService.EXTRA_WATCH_RADIUS, currentWatchCircleRadius) } + startService(intent) } buttonSetAnchor.setOnClickListener { - if (::locationService.isInitialized) { - lifecycleScope.launch { - locationService.startAnchorWatch(currentWatchCircleRadius) - Toast.makeText(this@MainActivity, "Anchor watch set!", Toast.LENGTH_SHORT).show() - } - } else { - Toast.makeText(this, "Location service not initialized. Grant permissions first.", Toast.LENGTH_LONG).show() + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_START_ANCHOR_WATCH + putExtra(LocationService.EXTRA_WATCH_RADIUS, currentWatchCircleRadius) } + startService(intent) + Toast.makeText(this@MainActivity, "Anchor watch set!", Toast.LENGTH_SHORT).show() } buttonStopAnchor.setOnClickListener { - if (::locationService.isInitialized) { - locationService.stopAnchorWatch() - Toast.makeText(this@MainActivity, "Anchor watch stopped.", Toast.LENGTH_SHORT).show() + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_STOP_ANCHOR_WATCH } + startService(intent) + Toast.makeText(this@MainActivity, "Anchor watch stopped.", Toast.LENGTH_SHORT).show() } mobRecoveredButton.setOnClickListener { @@ -303,6 +323,24 @@ class MainActivity : AppCompatActivity() { } } + private fun startLocationService() { + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_START_FOREGROUND_SERVICE + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + } + + private fun stopLocationService() { + val intent = Intent(this, LocationService::class.java).apply { + action = LocationService.ACTION_STOP_FOREGROUND_SERVICE + } + stopService(intent) + } + private fun createMockPolarTable(): PolarTable { // Example polar data for a hypothetical boat // TWS 6 knots @@ -416,14 +454,15 @@ class MainActivity : AppCompatActivity() { val angle = 2 * Math.PI * i / steps val lat = center.latitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * cos(angle) val lon = center.longitude() + (radiusMeters / earthRadius) * (180 / Math.PI) * sin(angle) / cos(toRadians(center.latitude())) - coordinates.add(Point.fromLngLat(lon, lat)) + coordinates.add(Point.fromLngLats(lon, lat)) } return Polygon.fromLngLats(listOf(coordinates)) } private fun observeLocationUpdates() { lifecycleScope.launch { - locationService.getLocationUpdates().distinctUntilChanged().collect { gpsData -> + // Observe from the static locationFlow in LocationService + LocationService.locationFlow.distinctUntilChanged().collect { gpsData -> if (mobActivated && activeMobWaypoint != null) { val mobLocation = Location("").apply { latitude = activeMobWaypoint!!.latitude @@ -449,14 +488,17 @@ class MainActivity : AppCompatActivity() { private fun observeAnchorWatchState() { lifecycleScope.launch { - locationService.anchorWatchState.collect { state -> + // Observe from the static anchorWatchState in LocationService + LocationService.anchorWatchState.collect { state -> withContext(Dispatchers.Main) { updateAnchorMapLayers(state) // Update map layers if (state.isActive && state.anchorLocation != null) { currentWatchCircleRadius = state.watchCircleRadiusMeters anchorRadiusText.text = String.format(Locale.getDefault(), "Radius: %.1fm", currentWatchCircleRadius) - locationService.fusedLocationClient.lastLocation.await()?.let { currentLocation -> + // Get the current location from the static flow + val currentLocation = LocationService.locationFlow.firstOrNull()?.toLocation() + if (currentLocation != null) { val distance = state.anchorLocation.distanceTo(currentLocation) val distanceDiff = distance - state.watchCircleRadiusMeters if (distanceDiff > 0) { @@ -482,6 +524,9 @@ class MainActivity : AppCompatActivity() { ) anchorStatusText.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.instrument_text_normal)) } + } else { + anchorStatusText.text = "Anchor watch active (waiting for location...)" + anchorStatusText.setTextColor(ContextCompat.getColor(this@MainActivity, R.color.instrument_text_normal)) } } else { anchorStatusText.text = getString(R.string.anchor_inactive) @@ -493,50 +538,41 @@ class MainActivity : AppCompatActivity() { } private fun activateMob() { - if (::locationService.isInitialized) { - CoroutineScope(Dispatchers.Main).launch { - try { - val lastLocation: Location? = locationService.fusedLocationClient.lastLocation.await() - if (lastLocation != null) { - activeMobWaypoint = MobWaypoint( - latitude = lastLocation.latitude, - longitude = lastLocation.longitude, - timestamp = System.currentTimeMillis() - ) - mobActivated = true - Log.d("MainActivity", "MOB Activated! Location: ${activeMobWaypoint!!.latitude}, ${activeMobWaypoint!!.longitude} at ${activeMobWaypoint!!.timestamp}") - Toast.makeText(this@MainActivity, "MOB Activated!", Toast.LENGTH_SHORT).show() - - // Switch display to MOB navigation view - mapView?.visibility = View.GONE - instrumentDisplayContainer.visibility = View.GONE - fabToggleInstruments.visibility = View.GONE - fabMob.visibility = View.GONE - anchorConfigContainer.visibility = View.GONE // Hide anchor config - fabAnchor.visibility = View.GONE // Hide anchor FAB - mobNavigationContainer.visibility = View.VISIBLE - - - // Sound continuous alarm - mobMediaPlayer = MediaPlayer.create(this@MainActivity, R.raw.mob_alarm).apply { - isLooping = true - start() - } + // Get last known location from the static flow + lifecycleScope.launch { + val lastGpsData: GpsData? = LocationService.locationFlow.firstOrNull() + if (lastGpsData != null) { + activeMobWaypoint = MobWaypoint( + latitude = lastGpsData.latitude, + longitude = lastGpsData.longitude, + timestamp = System.currentTimeMillis() + ) + mobActivated = true + Log.d("MainActivity", "MOB Activated! Location: ${activeMobWaypoint!!.latitude}, ${activeMobWaypoint!!.longitude} at ${activeMobWaypoint!!.timestamp}") + Toast.makeText(this@MainActivity, "MOB Activated!", Toast.LENGTH_SHORT).show() - // Log event to logbook - logMobEvent(activeMobWaypoint!!) - } else { - Toast.makeText(this@MainActivity, "Could not get current location for MOB", Toast.LENGTH_SHORT).show() - Log.e("MainActivity", "Last known location is null, cannot activate MOB.") - } - } catch (e: Exception) { - Toast.makeText(this@MainActivity, "Error getting location for MOB: ${e.message}", Toast.LENGTH_LONG).show() - Log.e("MainActivity", "Error getting location for MOB", e) + // Switch display to MOB navigation view + mapView?.visibility = View.GONE + instrumentDisplayContainer.visibility = View.GONE + fabToggleInstruments.visibility = View.GONE + fabMob.visibility = View.GONE + anchorConfigContainer.visibility = View.GONE // Hide anchor config + fabAnchor.visibility = View.GONE // Hide anchor FAB + mobNavigationContainer.visibility = View.VISIBLE + + + // Sound continuous alarm + mobMediaPlayer = MediaPlayer.create(this@MainActivity, R.raw.mob_alarm).apply { + isLooping = true + start() } + + // Log event to logbook + logMobEvent(activeMobWaypoint!!) + } else { + Toast.makeText(this@MainActivity, "Could not get current location for MOB", Toast.LENGTH_SHORT).show() + Log.e("MainActivity", "Last known location is null, cannot activate MOB.") } - } else { - Toast.makeText(this, "Location service not initialized. Grant permissions first.", Toast.LENGTH_LONG).show() - Log.e("MainActivity", "Location service not initialized when trying to activate MOB.") } } @@ -634,4 +670,4 @@ class MainActivity : AppCompatActivity() { mapView?.onDestroy() mobMediaPlayer?.release() // Ensure media player is released on destroy } -}
\ No newline at end of file +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/PolarData.kt b/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt index 395b80f..9624607 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/PolarData.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/PolarData.kt @@ -1,4 +1,4 @@ -package com.example.androidapp +package org.terst.nav import kotlin.math.abs import kotlin.math.cos diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/PolarDiagramView.kt b/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt index 36e7071..a794ed5 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/PolarDiagramView.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/PolarDiagramView.kt @@ -1,4 +1,4 @@ -package com.example.androidapp +package org.terst.nav import android.content.Context import android.graphics.Canvas diff --git a/android-app/app/src/main/res/drawable/ic_anchor.xml b/android-app/app/src/main/res/drawable/ic_anchor.xml new file mode 100644 index 0000000..2389c93 --- /dev/null +++ b/android-app/app/src/main/res/drawable/ic_anchor.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2S13.1,14 12,14z" /> +</vector> diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 4f38772..88944b8 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -210,7 +210,7 @@ app:layout_constraintHorizontal_bias="0.5" /> <!-- Polar Diagram View --> - <com.example.androidapp.PolarDiagramView + <org.terst.nav.PolarDiagramView android:id="@+id/polar_diagram_view" android:layout_width="0dp" android:layout_height="0dp" |
