diff options
Diffstat (limited to 'android-app/app/src')
7 files changed, 631 insertions, 2 deletions
diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorAlarmManager.kt b/android-app/app/src/main/kotlin/com/example/androidapp/AnchorAlarmManager.kt new file mode 100644 index 0000000..4b31719 --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/AnchorAlarmManager.kt @@ -0,0 +1,108 @@ +package com.example.androidapp + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.media.AudioAttributes +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager // For API 31+ +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat + +class AnchorAlarmManager(private val context: Context) { + + private val CHANNEL_ID = "anchor_alarm_channel" + private val NOTIFICATION_ID = 1001 + + private var isAlarming: Boolean = false + private var ringtone: android.media.Ringtone? = null + + init { + createNotificationChannel() + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Anchor Alarm" + val descriptionText = "Notifications for anchor drag events" + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel(CHANNEL_ID, name, importance).apply { + description = descriptionText + } + val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + @Suppress("DEPRECATION") + private fun getVibrator(): Vibrator? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + } + + fun startAlarm() { + if (isAlarming) return + + isAlarming = true + // Play sound + try { + val alarmUri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + ringtone = RingtoneManager.getRingtone(context, alarmUri) + ringtone?.audioAttributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_ALARM) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ringtone?.play() + } catch (e: Exception) { + e.printStackTrace() + } + + // Vibrate + val vibrator = getVibrator() + if (vibrator?.hasVibrator() == true) { + val pattern = longArrayOf(0, 1000, 1000) // Start immediately, vibrate for 1s, pause for 1s + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createWaveform(pattern, 0)) // Repeat indefinitely + } else { + vibrator.vibrate(pattern, 0) // Repeat indefinitely + } + } + + // Show persistent notification + showNotification("Anchor Drag Detected!", "Your boat is outside the watch circle.") + } + + fun stopAlarm() { + if (!isAlarming) return + + isAlarming = false + ringtone?.stop() + getVibrator()?.cancel() + NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID) + } + + private fun showNotification(title: String, message: String) { + val builder = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_alert) // Replace with a proper icon + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setOngoing(true) // Makes the notification persistent + .setAutoCancel(false) // Does not disappear when tapped + .setDefaults(NotificationCompat.DEFAULT_ALL) // Use default sound, vibrate, light (though we manually control sound/vibration) + + with(NotificationManagerCompat.from(context)) { + notify(NOTIFICATION_ID, builder.build()) + } + } +} diff --git a/android-app/app/src/main/kotlin/com/example/androidapp/AnchorWatchData.kt b/android-app/app/src/main/kotlin/com/example/androidapp/AnchorWatchData.kt new file mode 100644 index 0000000..c7c13fd --- /dev/null +++ b/android-app/app/src/main/kotlin/com/example/androidapp/AnchorWatchData.kt @@ -0,0 +1,22 @@ +package com.example.androidapp + +import android.location.Location + +data class AnchorWatchState( + val anchorLocation: Location? = null, + val watchCircleRadiusMeters: Double = DEFAULT_WATCH_CIRCLE_RADIUS_METERS, + val setTimeMillis: Long = 0L, + val isActive: Boolean = false +) { + companion object { + const val DEFAULT_WATCH_CIRCLE_RADIUS_METERS = 50.0 // Default 50 meters + } + + fun isDragging(currentLocation: Location): Boolean { + anchorLocation ?: return false // Cannot drag if anchor not set + if (!isActive) return false // Not active, so not dragging + + val distance = anchorLocation.distanceTo(currentLocation) + return distance > watchCircleRadiusMeters + } +} 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 index 346fdfe..ca73397 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/LocationService.kt +++ b/android-app/app/src/main/kotlin/com/example/androidapp/LocationService.kt @@ -7,20 +7,42 @@ 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) @@ -37,6 +59,33 @@ class LocationService(private val context: Context) { 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) + } } } } @@ -51,4 +100,43 @@ class LocationService(private val context: Context) { 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/MainActivity.kt b/android-app/app/src/main/kotlin/com/example/androidapp/MainActivity.kt index d4c6998..5a91a7a 100644 --- a/android-app/app/src/main/kotlin/com/example/androidapp/MainActivity.kt +++ b/android-app/app/src/main/kotlin/com/example/androidapp/MainActivity.kt @@ -1,20 +1,59 @@ package com.example.androidapp +import android.Manifest +import android.content.pm.PackageManager +import android.location.Location import android.os.Bundle +import android.util.Log import android.view.View +import android.widget.Button import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.google.android.material.floatingactionbutton.FloatingActionButton import org.maplibre.android.MapLibre import org.maplibre.android.maps.MapView import org.maplibre.android.maps.Style +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.Locale +import java.util.concurrent.TimeUnit + +data class MobWaypoint( + val latitude: Double, + val longitude: Double, + val timestamp: Long // System.currentTimeMillis() +) class MainActivity : AppCompatActivity() { private var mapView: MapView? = null private lateinit var instrumentDisplayContainer: ConstraintLayout private lateinit var fabToggleInstruments: FloatingActionButton + private lateinit var fabMob: FloatingActionButton + + // MOB UI elements + private lateinit var mobNavigationContainer: ConstraintLayout + private lateinit var mobValueDistance: TextView + private lateinit var mobValueElapsedTime: TextView + private lateinit var mobRecoveredButton: Button + + private lateinit var locationService: LocationService + + // MOB State + private var mobActivated: Boolean = false + private var activeMobWaypoint: MobWaypoint? = null + + // Media player for MOB alarm + private var mobMediaPlayer: MediaPlayer? = null // Instrument TextViews private lateinit var valueAws: TextView @@ -27,6 +66,22 @@ class MainActivity : AppCompatActivity() { private lateinit var valueDepth: TextView private lateinit var valuePolarPct: TextView + // Register the permissions callback, which handles the user's response to the + // 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 + Toast.makeText(this, "Location permissions granted", Toast.LENGTH_SHORT).show() + locationService = LocationService(this) + observeLocationUpdates() // Start observing location updates + } else { + // Permissions denied, handle the case (e.g., show a message to the user) + Toast.makeText(this, "Location permissions denied", Toast.LENGTH_LONG).show() + Log.e("MainActivity", "Location permissions denied by user.") + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,6 +89,19 @@ class MainActivity : AppCompatActivity() { MapLibre.getInstance(this) setContentView(R.layout.activity_main) + // 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 + )) + } else { + // Permissions already granted, initialize location service + locationService = LocationService(this) + observeLocationUpdates() // Start observing location updates + } + mapView = findViewById(R.id.mapView) mapView?.onCreate(savedInstanceState) mapView?.getMapAsync { maplibreMap -> @@ -42,6 +110,13 @@ class MainActivity : AppCompatActivity() { instrumentDisplayContainer = findViewById(R.id.instrument_display_container) fabToggleInstruments = findViewById(R.id.fab_toggle_instruments) + fabMob = findViewById(R.id.fab_mob) + + // Initialize MOB UI elements + mobNavigationContainer = findViewById(R.id.mob_navigation_container) + mobValueDistance = findViewById(R.id.mob_value_distance) + mobValueElapsedTime = findViewById(R.id.mob_value_elapsed_time) + mobRecoveredButton = findViewById(R.id.mob_recovered_button) // Initialize instrument TextViews valueAws = findViewById(R.id.value_aws) @@ -76,6 +151,120 @@ class MainActivity : AppCompatActivity() { mapView?.visibility = View.GONE } } + + fabMob.setOnClickListener { + activateMob() + } + + mobRecoveredButton.setOnClickListener { + recoverMob() + } + } + + private fun observeLocationUpdates() { + lifecycleScope.launch { + locationService.getLocationUpdates().distinctUntilChanged().collect { gpsData -> + if (mobActivated && activeMobWaypoint != null) { + val mobLocation = Location("").apply { + latitude = activeMobWaypoint!!.latitude + longitude = activeMobWaypoint!!.longitude + } + val currentPosition = Location("").apply { + latitude = gpsData.latitude + longitude = gpsData.longitude + } + + val distance = currentPosition.distanceTo(mobLocation) // distance in meters + val elapsedTime = System.currentTimeMillis() - activeMobWaypoint!!.timestamp + + withContext(Dispatchers.Main) { + mobValueDistance.text = String.format(Locale.getDefault(), "%.1f m", distance) + mobValueElapsedTime.text = formatElapsedTime(elapsedTime) + // TODO: Update bearing arrow (requires custom view or rotation logic) + } + } + } + } + } + + 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 + 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.") + } + } 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) + } + } + } 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.") + } + } + + private fun recoverMob() { + mobActivated = false + activeMobWaypoint = null + stopMobAlarm() + + mobNavigationContainer.visibility = View.GONE + mapView?.visibility = View.VISIBLE + // instrumentDisplayContainer visibility is controlled by fabToggleInstruments, so leave as is + fabToggleInstruments.visibility = View.VISIBLE + fabMob.visibility = View.VISIBLE + + Toast.makeText(this, "MOB Recovery initiated.", Toast.LENGTH_SHORT).show() + Log.d("MainActivity", "MOB Recovery initiated.") + } + + private fun stopMobAlarm() { + mobMediaPlayer?.stop() + mobMediaPlayer?.release() + mobMediaPlayer = null + Log.d("MainActivity", "MOB Alarm stopped and released.") + } + + private fun logMobEvent(mobWaypoint: MobWaypoint) { + Log.i("Logbook", "MOB Event: Lat ${mobWaypoint.latitude}, Lon ${mobWaypoint.longitude}, Time ${mobWaypoint.timestamp}") + // TODO: Integrate with actual logbook system for persistence + } + + + private fun formatElapsedTime(milliseconds: Long): String { + val hours = TimeUnit.MILLISECONDS.toHours(milliseconds) + val minutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds) % 60 + val seconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds) % 60 + return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds) } private fun updateInstrumentDisplay( @@ -133,5 +322,6 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { super.onDestroy() mapView?.onDestroy() + mobMediaPlayer?.release() // Ensure media player is released on destroy } } 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 2801f23..3df0645 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -43,7 +43,7 @@ <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline_horizontal_50" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android="layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.5" /> @@ -241,4 +241,207 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> + <!-- Anchor FAB --> + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/fab_anchor" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:clickable="true" + android:focusable="true" + android:contentDescription="@string/fab_anchor_content_description" + app:srcCompat="@android:drawable/ic_menu_myplaces" + app:backgroundTint="@color/anchor_button_background" + app:layout_constraintBottom_toTopOf="@+id/fab_mob" + app:layout_constraintStart_toStartOf="parent" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/fab_mob" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:clickable="true" + android:focusable="true" + android:contentDescription="@string/fab_mob_content_description" + app:srcCompat="@android:drawable/ic_dialog_alert" + app:backgroundTint="@color/mob_button_background" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + + <!-- Anchor Configuration Container --> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/anchor_config_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="#DD212121" + android:padding="16dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <TextView + android:id="@+id/anchor_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/anchor_config_title" + android:textColor="@android:color/white" + android:textSize="20sp" + android:textStyle="bold" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/anchor_status_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:textColor="@android:color/white" + android:textSize="16sp" + tools:text="Anchor Inactive" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/anchor_title" /> + + <LinearLayout + android:id="@+id/radius_control_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="16dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/anchor_status_text"> + + <Button + android:id="@+id/button_decrease_radius" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="-" + android:textSize="20sp" + android:minWidth="48dp" + android:layout_marginEnd="8dp" /> + + <TextView + android:id="@+id/anchor_radius_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@android:color/white" + android:textSize="18sp" + android:textStyle="bold" + tools:text="Radius: 50.0m" + android:gravity="center_vertical" /> + + <Button + android:id="@+id/button_increase_radius" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="+" + android:textSize="20sp" + android:minWidth="48dp" + android:layout_marginStart="8dp" /> + + </LinearLayout> + + <Button + android:id="@+id/button_set_anchor" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/button_set_anchor" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/radius_control_layout" /> + + <Button + android:id="@+id/button_stop_anchor" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/button_stop_anchor" + android:backgroundTint="@android:color/holo_red_dark" + app:layout_constraintStart_toEndOf="@+id/button_set_anchor" + app:layout_constraintTop_toBottomOf="@+id/radius_control_layout" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <!-- MOB Navigation Display Container --> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/mob_navigation_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/instrument_background" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/mob_label_distance" + style="@style/InstrumentLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/mob_label_distance" + app:layout_constraintBottom_toTopOf="@+id/mob_value_distance" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/mob_value_distance" + style="@style/InstrumentPrimaryValue" + android:layout_width="wrap_content" + android://layout_height="wrap_content" + tools:text="125 m" + android:textSize="80sp" + app:layout_constraintBottom_toTopOf="@+id/mob_label_elapsed_time" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/mob_label_distance" /> + + <TextView + android:id="@+id/mob_label_elapsed_time" + style="@style/InstrumentLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:text="@string/mob_label_elapsed_time" + app:layout_constraintBottom_toTopOf="@+id/mob_value_elapsed_time" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/mob_value_distance" /> + + <TextView + android:id="@+id/mob_value_elapsed_time" + style="@style/InstrumentPrimaryValue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:text="00:01:23" + android:textSize="60sp" + app:layout_constraintBottom_toTopOf="@+id/mob_recovered_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/mob_label_elapsed_time" /> + + <Button + android:id="@+id/mob_recovered_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="64dp" + android:text="@string/mob_button_recovered" + android:paddingStart="32dp" + android:paddingEnd="32dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:textSize="24sp" + android:backgroundTint="@color/mob_button_background" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/mob_value_elapsed_time" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/android-app/app/src/main/res/values/colors.xml b/android-app/app/src/main/res/values/colors.xml index a66628b..3dce53c 100644 --- a/android-app/app/src/main/res/values/colors.xml +++ b/android-app/app/src/main/res/values/colors.xml @@ -12,4 +12,6 @@ <color name="instrument_text_alarm">#FFFF0000</color> <!-- Red for alarm --> <color name="instrument_text_stale">#FFFFFF00</color> <!-- Yellow for stale data --> <color name="instrument_background">#E61E1E1E</color> <!-- Slightly transparent dark grey --> + <color name="mob_button_background">#FFD70000</color> <!-- High-contrast red for MOB button --> + <color name="anchor_button_background">#3F51B5</color> </resources>
\ No newline at end of file diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml index d7793de..44f67ea 100644 --- a/android-app/app/src/main/res/values/strings.xml +++ b/android-app/app/src/main/res/values/strings.xml @@ -25,4 +25,20 @@ <string name="placeholder_vmg_value">--.-</string> <string name="placeholder_depth_value">--.-</string> <string name="placeholder_polar_value">---</string> + + <string name="fab_mob_content_description">Activate Man Overboard (MOB) alarm</string> + <string name="fab_anchor_content_description">Toggle Anchor Watch Configuration</string> + + <!-- MOB Navigation View Strings --> + <string name="mob_label_distance">DISTANCE TO MOB</string> + <string name="mob_label_elapsed_time">ELAPSED TIME</string> + <string name="mob_button_recovered">RECOVERED</string> + + <!-- Anchor Watch Strings --> + <string name="anchor_config_title">Anchor Watch</string> + <string name="button_set_anchor">SET ANCHOR</string> + <string name="button_stop_anchor">STOP WATCH</string> + <string name="anchor_inactive">Anchor Watch Inactive</string> + <string name="anchor_active_format">Anchor Set at %.4f, %.4f\nRadius: %.1fm\nDistance: %.1fm (%.1fm from limit)</string> + <string name="anchor_active_dragging_format">!!! ANCHOR DRAG !!!\nAnchor Set at %.4f, %.4f\nRadius: %.1fm\nDistance: %.1fm (%.1fm OVER limit)</string> </resources>
\ No newline at end of file |
