diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-06-30 17:16:06 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-06-30 18:19:04 +0000 |
| commit | 9af38934e8d676bfb8c08ab01e3d57fec5f36d97 (patch) | |
| tree | 67b5bdfffb596d63fb39d17465704bb97ad6bf8a /android-app/app | |
| parent | 36efb5a8b9502d0def41c0cdc17ed894744c8562 (diff) | |
feat: UX improvements + HAW FAD data + fishing mode plan
Dark mode:
- Force light mode (AppCompatDelegate.MODE_NIGHT_NO) in NavApplication
Log screen:
- Tracks sorted newest-first (sortedByDescending startMs)
- Photos/entries saved during a track tagged with trackId; ship's log
shows only standalone entries (trackId == null)
- "Plan Trip" button moved from safety screen to log screen header row
- Pre-trip report auto-generates (newAutoInstance) when a track starts
Safety screen:
- Removed "Plan Trip" button (now in log screen)
- Quit button moved to top title row
- Anchor Watch section collapses by default (tap header to expand)
- Hardware Data Sources section collapses by default (tap header to expand)
Notifications + idle:
- Tracking notification becomes persistent (setOngoing true) while recording
- Notification title changes to "Recording track" when active
- App exits automatically after 3 hours of idle (resetIdleTimer via
onUserInteraction + onResume; suspended while recording)
FAD layer:
- FadData.kt: 18 Hawaii Island DLNR FAD coordinates hardcoded
(source: DLNR Division of Aquatic Resources 6/14/07)
- FAD GeoJSON populated on style load from static data
Fishing mode plan:
- docs/superpowers/plans/2026-06-30-fishing-mode.md: 7-task plan for
RigAdvisor, SST tiles, FishingModeManager, FishingOverlayView, steering
bearing line, and FAB integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app')
12 files changed, 462 insertions, 264 deletions
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 index c1ff60e..3c89fc5 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/LocationService.kt @@ -301,6 +301,11 @@ class LocationService : Service() { Log.d("LocationService", "Stopping NMEA stream") nmeaStreamManager.stop() } + ACTION_SET_RECORDING -> { + val recording = intent.getBooleanExtra(EXTRA_IS_RECORDING, false) + val mgr = getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager + mgr.notify(NOTIFICATION_ID, createNotification(recording)) + } } return START_NOT_STICKY } @@ -369,14 +374,17 @@ class LocationService : Service() { manager.createNotificationChannel(serviceChannel) } - private fun createNotification(): Notification { + private fun createNotification(recording: Boolean = false): Notification { val notificationIntent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) + val title = if (recording) "Recording track" else "Sailing Companion" + val text = if (recording) "Track recording in progress — tap to open" else "Tracking your location..." return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setContentTitle("Sailing Companion") - .setContentText("Tracking your location...") + .setContentTitle(title) + .setContentText(text) .setSmallIcon(R.drawable.ic_anchor) .setContentIntent(pendingIntent) + .setOngoing(recording) .build() } @@ -425,8 +433,10 @@ class LocationService : Service() { const val ACTION_TOGGLE_TIDAL_VISIBILITY = "ACTION_TOGGLE_TIDAL_VISIBILITY" const val ACTION_START_NMEA = "ACTION_START_NMEA" const val ACTION_STOP_NMEA = "ACTION_STOP_NMEA" + const val ACTION_SET_RECORDING = "ACTION_SET_RECORDING" const val EXTRA_TIDAL_VISIBILITY = "extra_tidal_visibility" const val EXTRA_WATCH_RADIUS = "extra_watch_radius" + const val EXTRA_IS_RECORDING = "extra_is_recording" private val _locationFlow = MutableSharedFlow<GpsPosition>(replay = 1) val locationFlow: SharedFlow<GpsPosition> get() = _locationFlow diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index ea45c36..fcfc96a 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -79,6 +79,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private var weatherLoaded = false private var windGridLoaded = false private var cameraIdleJob: Job? = null + private var idleJob: Job? = null private lateinit var bottomSheetBehavior: BottomSheetBehavior<View> private lateinit var fragmentContainer: FrameLayout @@ -136,6 +137,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } else { restoreLocationMode() } + resetIdleTimer() } override fun onPause() { @@ -144,6 +146,20 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { throttleLocationIfIdle() } + override fun onUserInteraction() { + super.onUserInteraction() + resetIdleTimer() + } + + private fun resetIdleTimer() { + idleJob?.cancel() + if (viewModel.isRecording.value) return // never time out while recording + idleJob = lifecycleScope.launch { + delay(3 * 60 * 60 * 1000L) // 3 hours of no interaction + exitApp() + } + } + override fun onCreate(savedInstanceState: Bundle?) { if (isNightMode) setTheme(R.style.Theme_Nav_NightVision) super.onCreate(savedInstanceState) @@ -266,7 +282,10 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { } fabRecordTrack.setOnClickListener { - if (!viewModel.isRecording.value) viewModel.startTrack() + if (!viewModel.isRecording.value) { + viewModel.startTrack() + showOverlay(org.terst.nav.tripreport.PreTripReportFragment.newAutoInstance()) + } } val tvTrackStats = findViewById<TextView>(R.id.tv_track_stats) @@ -665,6 +684,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap, userBitmap) mapHandler?.setupWindLayer(style, windArrowBitmap) mapHandler?.setupFadLayer(style, layerManager.fadsEnabled) + mapHandler?.updateFadLayer(FadData.toFeatures()) // Eagerly load conditions at style-ready time so the HUD is never blank // on startup — the camera-idle may fire before GPS fixes, this is the backstop. maplibreMap.cameraPosition.target?.let { center -> @@ -758,6 +778,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { val action = if (recording) LocationService.ACTION_START_FULL else LocationService.ACTION_START_ECONOMY startService(locationServiceIntent(action)) + // Update foreground notification to show recording state + startService(Intent(this@MainActivity, LocationService::class.java).apply { + this.action = LocationService.ACTION_SET_RECORDING + putExtra(LocationService.EXTRA_IS_RECORDING, recording) + }) + // Recording keeps the idle timer suspended; non-recording restarts it + resetIdleTimer() } } var conditionsLoaded = false diff --git a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt index 3873002..8e6bdfa 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/NavApplication.kt @@ -1,6 +1,7 @@ package org.terst.nav import android.app.Application +import androidx.appcompat.app.AppCompatDelegate import com.google.firebase.crashlytics.FirebaseCrashlytics import org.terst.nav.db.NavDatabase import org.terst.nav.settings.FeatureFlags @@ -38,6 +39,7 @@ class NavApplication : Application() { override fun onCreate() { super.onCreate() + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) featureFlags = FeatureFlags(this) database = NavDatabase.get(this) logbookRepository = org.terst.nav.logbook.LogbookRepository(org.terst.nav.logbook.LogbookStorage(this)) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt index c038547..46c8869 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/LogEntry.kt @@ -9,5 +9,6 @@ data class LogEntry( val entryType: EntryType, val lat: Double? = null, val lon: Double? = null, - val photoPath: String? = null // absolute file path or content URI string + val photoPath: String? = null, // absolute file path or content URI string + val trackId: Long? = null // non-null when entry belongs to a recorded track ) diff --git a/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt index 67e1062..833a42b 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/logbook/VoiceLogViewModel.kt @@ -8,7 +8,7 @@ class VoiceLogViewModel(private val repository: LogbookRepository) { private val _state = MutableStateFlow<VoiceLogState>(VoiceLogState.Idle) val state: StateFlow<VoiceLogState> = _state - private val _entries = MutableStateFlow<List<LogEntry>>(repository.getAll().reversed()) + private val _entries = MutableStateFlow<List<LogEntry>>(standalonEntries()) val entries: StateFlow<List<LogEntry>> = _entries fun onListeningStarted() { @@ -23,7 +23,7 @@ class VoiceLogViewModel(private val repository: LogbookRepository) { _state.value = VoiceLogState.Error(message) } - fun save(text: String, photoPath: String? = null, lat: Double? = null, lon: Double? = null) { + fun save(text: String, photoPath: String? = null, lat: Double? = null, lon: Double? = null, trackId: Long? = null) { if (text.isBlank() && photoPath == null) return val entry = LogEntry( timestampMs = System.currentTimeMillis(), @@ -31,14 +31,18 @@ class VoiceLogViewModel(private val repository: LogbookRepository) { entryType = EntryType.GENERAL, lat = lat, lon = lon, - photoPath = photoPath + photoPath = photoPath, + trackId = trackId ) val saved = repository.save(entry) - _entries.value = repository.getAll().reversed() + _entries.value = standalonEntries() _state.value = VoiceLogState.Saved(saved) } fun retry() { _state.value = VoiceLogState.Idle } + + private fun standalonEntries() = + repository.getAll().filter { it.trackId == null }.reversed() } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 4648981..61a63eb 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -165,7 +165,7 @@ class TrackRepository(context: Context, private val trackDao: TrackDao) { .onFailure { e -> NavLogger.e("track", "toSavedTrack failed: ${e.javaClass.simpleName}: ${e.message}") } .getOrNull() } - .sortedBy { it.startMs } + .sortedByDescending { it.startMs } } /** @@ -173,7 +173,7 @@ class TrackRepository(context: Context, private val trackDao: TrackDao) { * with only start/end placeholder points. Use [getSavedTracks] for full point data. */ suspend fun getSavedTracksFromRoom(): List<SavedTrack> = withContext(Dispatchers.IO) { - trackDao.getAll().map { it.toShallowSavedTrack() }.sortedBy { it.startMs } + trackDao.getAll().map { it.toShallowSavedTrack() }.sortedByDescending { it.startMs } } fun safState(): SafState = storage.safState() diff --git a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt index ac1350d..9606398 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/tripreport/PreTripReportFragment.kt @@ -30,6 +30,15 @@ import java.util.Locale class PreTripReportFragment : Fragment() { + companion object { + private const val ARG_AUTO = "auto_generate" + + /** Opens pre-trip report and auto-generates from current conditions immediately. */ + fun newAutoInstance() = PreTripReportFragment().apply { + arguments = Bundle().also { it.putBoolean(ARG_AUTO, true) } + } + } + private val mainViewModel: MainViewModel by activityViewModels() private lateinit var viewModel: PreTripReportViewModel @@ -70,7 +79,8 @@ class PreTripReportFragment : Fragment() { btnGenerate.setOnClickListener { triggerGenerate() } btnPickDeparture.setOnClickListener { showDeparturePicker() } - if (mainViewModel.forecast.value.isNotEmpty()) { + val autoGenerate = arguments?.getBoolean(ARG_AUTO, false) == true + if (autoGenerate || mainViewModel.forecast.value.isNotEmpty()) { triggerGenerate() } } diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/FadData.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/FadData.kt new file mode 100644 index 0000000..0b3fac0 --- /dev/null +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/FadData.kt @@ -0,0 +1,42 @@ +package org.terst.nav.ui + +import org.maplibre.geojson.Feature +import org.maplibre.geojson.Point + +/** + * Hawaii Island DLNR Fish Aggregating Device (FAD) locations. + * Source: DLNR Division of Aquatic Resources — coordinates in decimal degrees. + * Contact: Hawaii Institute of Marine Biology / DAR 808-327-6226 (Kona), 808-974-6201 (Hilo). + */ +object FadData { + + data class Fad(val id: String, val lat: Double, val lon: Double, val depthFathoms: Int) + + val hawaiiIsland: List<Fad> = listOf( + Fad("A", 18.0 + 57.35 / 60, -(155.0 + 33.4 / 60), 700), // Ka Lae (South Pt.) + Fad("B", 19.0 + 11.9 / 60, -(155.0 + 56.9 / 60), 850), // Miloli'i + Fad("C", 19.0 + 23.1 / 60, -(155.0 + 59.2 / 60), 969), // Lae Loa (Loa Pt.) + Fad("D", 19.0 + 37.5 / 60, -(154.0 + 46.7 / 60), 950), // Kumukahi + Fad("E", 19.0 + 46.1 / 60, -(154.0 + 54.8 / 60), 920), // Lele'iwi + Fad("F", 19.0 + 30.4 / 60, -(156.0 + 9.4 / 60), 1592), // Kailua-Kona + Fad("G", 19.0 + 50.7 / 60, -(154.0 + 53.3 / 60), 578), // Pepe'ekeo + Fad("HK", 19.0 + 58.64 / 60, -(154.0 + 59.0 / 60), 890), // Hakalau + Fad("KH", 19.0 + 20.9 / 60, -(154.0 + 52.8 / 60), 940), // Kehena + Fad("OTEC", 19.0 + 52.6 / 60, -(156.0 + 11.6 / 60), 714), // Waikoloa OTEC + Fad("QQ", 19.0 + 39.2 / 60, -(154.0 + 53.5 / 60), 950), // Maku'u + Fad("RN", 19.0 + 7.8 / 60, -(155.0 + 23.5 / 60), 733), // Pālima Pt. + Fad("SS", 19.0 + 11.6 / 60, -(155.0 + 13.1 / 60), 515), // 'Āpua Pt. + Fad("TT", 19.0 + 4.6 / 60, -(155.0 + 57.4 / 60), 700), // Kānewa'a Pt. + Fad("UU", 19.0 + 16.8 / 60, -(155.0 + 57.1 / 60), 650), // 'Au'au Pt. + Fad("VV", 19.0 + 35.1 / 60, -(156.0 + 1.9 / 60), 600), // Kahalu'u + Fad("XX", 20.0 + 1.4 / 60, -(156.0 + 1.3 / 60), 345), // Puakō + Fad("ZZ", 19.0 + 56.9 / 60, -(155.0 + 57.7 / 60), 214), // Waimā Pt. + ) + + fun toFeatures(): List<Feature> = hawaiiIsland.map { fad -> + Feature.fromGeometry(Point.fromLngLat(fad.lon, fad.lat)).apply { + addStringProperty("id", fad.id) + addNumberProperty("depth_fathoms", fad.depthFathoms) + } + } +} diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt index c4b6e1e..e4b2d97 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/safety/SafetyFragment.kt @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.EditText +import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment @@ -55,8 +56,23 @@ class SafetyFragment : Fragment() { listener?.onActivateMob() } - val etDepth = view.findViewById<EditText>(R.id.et_anchor_depth) - val etRode = view.findViewById<EditText>(R.id.et_anchor_rode) + view.findViewById<MaterialButton>(R.id.button_quit).setOnClickListener { + listener?.onQuitRequested() + } + + // ── Anchor Watch — collapsible ──────────────────────────────────────── + val headerAnchor = view.findViewById<LinearLayout>(R.id.header_anchor) + val bodyAnchor = view.findViewById<LinearLayout>(R.id.body_anchor) + val chevronAnchor = view.findViewById<TextView>(R.id.tv_anchor_chevron) + + headerAnchor.setOnClickListener { + val expanding = bodyAnchor.visibility == View.GONE + bodyAnchor.visibility = if (expanding) View.VISIBLE else View.GONE + chevronAnchor.text = if (expanding) "▼" else "▶" + } + + val etDepth = view.findViewById<EditText>(R.id.et_anchor_depth) + val etRode = view.findViewById<EditText>(R.id.et_anchor_rode) val tvRadius = view.findViewById<TextView>(R.id.tv_anchor_suggested_radius) val watcher = object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit @@ -75,6 +91,17 @@ class SafetyFragment : Fragment() { etDepth.addTextChangedListener(watcher) etRode.addTextChangedListener(watcher) + // ── Hardware Data Sources — collapsible ─────────────────────────────── + val headerSources = view.findViewById<LinearLayout>(R.id.header_sources) + val bodySources = view.findViewById<LinearLayout>(R.id.body_sources) + val chevronSources = view.findViewById<TextView>(R.id.tv_sources_chevron) + + headerSources.setOnClickListener { + val expanding = bodySources.visibility == View.GONE + bodySources.visibility = if (expanding) View.VISIBLE else View.GONE + chevronSources.text = if (expanding) "▼" else "▶" + } + val flags = (requireActivity().application as NavApplication).featureFlags val switchAis = view.findViewById<MaterialSwitch>(R.id.switch_ais) @@ -90,17 +117,6 @@ class SafetyFragment : Fragment() { flags.setEnabled(HardwareSource.NMEA_INSTRUMENTS, checked) listener?.onHardwareSourceChanged(HardwareSource.NMEA_INSTRUMENTS, checked) } - - view.findViewById<MaterialButton>(R.id.button_plan_trip).setOnClickListener { - parentFragmentManager.beginTransaction() - .replace(R.id.fragment_container, org.terst.nav.tripreport.PreTripReportFragment()) - .addToBackStack(null) - .commit() - } - - view.findViewById<MaterialButton>(R.id.button_quit).setOnClickListener { - listener?.onQuitRequested() - } } fun updateAnchorStatus(statusText: String) { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt index 220b288..51a0685 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/voicelog/VoiceLogFragment.kt @@ -153,11 +153,13 @@ class VoiceLogFragment : Fragment() { btnSave.setOnClickListener { val pos = mainViewModel.currentPosition.value + val trackId = if (mainViewModel.isRecording.value) mainViewModel.trackStartMs else null logViewModel.save( text = etNote.text?.toString()?.trim() ?: "", photoPath = pendingPhotoPath, lat = pos?.first, - lon = pos?.second + lon = pos?.second, + trackId = trackId ) } btnClear.setOnClickListener { clearEntry() } @@ -186,6 +188,13 @@ class VoiceLogFragment : Fragment() { } } + view.findViewById<MaterialButton>(R.id.btn_plan_trip).setOnClickListener { + parentFragmentManager.beginTransaction() + .replace(R.id.fragment_container, org.terst.nav.tripreport.PreTripReportFragment()) + .addToBackStack(null) + .commit() + } + // Inline track list val rv = view.findViewById<RecyclerView>(R.id.rv_saved_tracks) val layoutEmpty = view.findViewById<View>(R.id.layout_empty) diff --git a/android-app/app/src/main/res/layout/fragment_safety.xml b/android-app/app/src/main/res/layout/fragment_safety.xml index 1d499ab..420ccef 100644 --- a/android-app/app/src/main/res/layout/fragment_safety.xml +++ b/android-app/app/src/main/res/layout/fragment_safety.xml @@ -1,335 +1,395 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.core.widget.NestedScrollView + xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="24dp" android:background="?attr/colorSurface"> - <TextView - android:id="@+id/safety_title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Safety Dashboard" - android:textSize="24sp" - android:textStyle="bold" - android:textColor="?attr/colorOnSurface" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/button_help_safety" - style="@style/Widget.Material3.Button.TextButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="HELP" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> - - <!-- MOB Section --> - <com.google.android.material.card.MaterialCardView - android:id="@+id/card_mob" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="24dp" - app:cardCornerRadius="16dp" - app:layout_constraintTop_toBottomOf="@id/safety_title"> + android:orientation="vertical" + android:padding="24dp"> + <!-- ── Title row ── --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp" - android:gravity="center"> + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="24dp"> <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Safety Dashboard" + android:textSize="24sp" + android:textStyle="bold" + android:textColor="?attr/colorOnSurface" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_quit" + style="@style/Widget.Material3.Button.TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="MAN OVERBOARD" - android:textColor="@color/md_theme_error" - android:textStyle="bold" /> + android:text="Quit" + android:textColor="?attr/colorOnSurfaceVariant" + app:icon="@drawable/ic_close" /> <com.google.android.material.button.MaterialButton - android:id="@+id/button_mob_activate" - android:layout_width="match_parent" - android:layout_height="80dp" - android:layout_marginTop="16dp" - android:text="ACTIVATE MOB" - android:textSize="20sp" - app:backgroundTint="@color/md_theme_error" /> + android:id="@+id/button_help_safety" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="HELP" /> </LinearLayout> - </com.google.android.material.card.MaterialCardView> - - <!-- CPA / Collision Alert Section --> - <com.google.android.material.card.MaterialCardView - android:id="@+id/card_cpa" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - app:cardCornerRadius="16dp" - app:layout_constraintTop_toBottomOf="@id/card_mob"> - - <LinearLayout + <!-- MOB Section --> + <com.google.android.material.card.MaterialCardView + android:id="@+id/card_mob" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="COLLISION AVOIDANCE" - android:textStyle="bold" /> + android:layout_marginBottom="16dp" + app:cardCornerRadius="16dp"> - <TextView - android:id="@+id/tv_cpa_status" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:text="No traffic alerts" - android:textColor="?attr/colorOnSurfaceVariant" - android:fontFamily="monospace" - android:textSize="13sp" /> + android:orientation="vertical" + android:padding="16dp" + android:gravity="center"> - </LinearLayout> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="MAN OVERBOARD" + android:textColor="@color/md_theme_error" + android:textStyle="bold" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button_mob_activate" + android:layout_width="match_parent" + android:layout_height="80dp" + android:layout_marginTop="16dp" + android:text="ACTIVATE MOB" + android:textSize="20sp" + app:backgroundTint="@color/md_theme_error" /> - </com.google.android.material.card.MaterialCardView> + </LinearLayout> - <!-- Anchor Section --> - <com.google.android.material.card.MaterialCardView - android:id="@+id/card_anchor" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - app:cardCornerRadius="16dp" - app:layout_constraintTop_toBottomOf="@id/card_cpa"> + </com.google.android.material.card.MaterialCardView> - <LinearLayout + <!-- CPA / Collision Alert Section --> + <com.google.android.material.card.MaterialCardView + android:id="@+id/card_cpa" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp"> + android:layout_marginBottom="16dp" + app:cardCornerRadius="16dp"> - <TextView - android:layout_width="wrap_content" + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="ANCHOR WATCH" - android:textStyle="bold" /> + android:orientation="vertical" + android:padding="16dp"> - <TextView - android:id="@+id/anchor_status_summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="8dp" - android:text="Status: Inactive" - android:textColor="?attr/colorOnSurfaceVariant" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="COLLISION AVOIDANCE" + android:textStyle="bold" /> + + <TextView + android:id="@+id/tv_cpa_status" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="No traffic alerts" + android:textColor="?attr/colorOnSurfaceVariant" + android:fontFamily="monospace" + android:textSize="13sp" /> + + </LinearLayout> + + </com.google.android.material.card.MaterialCardView> + + <!-- Anchor Section (collapsible) --> + <com.google.android.material.card.MaterialCardView + android:id="@+id/card_anchor" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + app:cardCornerRadius="16dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" - android:layout_marginTop="16dp"> + android:orientation="vertical" + android:padding="16dp"> + <!-- Collapsible header --> <LinearLayout - android:layout_width="0dp" + android:id="@+id/header_anchor" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical" - android:layout_marginEnd="8dp"> + android:orientation="horizontal" + android:gravity="center_vertical" + android:clickable="true" + android:focusable="true"> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="ANCHOR WATCH" + android:textStyle="bold" /> <TextView + android:id="@+id/tv_anchor_chevron" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/anchor_depth_label" + android:text="▶" android:textSize="12sp" android:textColor="?attr/colorOnSurfaceVariant" /> - <EditText - android:id="@+id/et_anchor_depth" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:hint="@string/anchor_depth_hint" - android:inputType="numberDecimal" - android:importantForAutofill="no" /> - </LinearLayout> + <!-- Collapsible body — hidden by default --> <LinearLayout - android:layout_width="0dp" + android:id="@+id/body_anchor" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" android:orientation="vertical" - android:layout_marginStart="8dp"> + android:visibility="gone"> <TextView + android:id="@+id/anchor_status_summary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/anchor_rode_label" - android:textSize="12sp" + android:layout_marginTop="8dp" + android:text="Status: Inactive" android:textColor="?attr/colorOnSurfaceVariant" /> - <EditText - android:id="@+id/et_anchor_rode" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/anchor_rode_hint" - android:inputType="numberDecimal" - android:importantForAutofill="no" /> - - </LinearLayout> - - </LinearLayout> + android:orientation="horizontal" + android:layout_marginTop="16dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginEnd="8dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/anchor_depth_label" + android:textSize="12sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + <EditText + android:id="@+id/et_anchor_depth" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/anchor_depth_hint" + android:inputType="numberDecimal" + android:importantForAutofill="no" /> + + </LinearLayout> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginStart="8dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/anchor_rode_label" + android:textSize="12sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + <EditText + android:id="@+id/et_anchor_rode" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/anchor_rode_hint" + android:inputType="numberDecimal" + android:importantForAutofill="no" /> + + </LinearLayout> + + </LinearLayout> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:text="@string/anchor_suggested_radius_label" - android:textSize="12sp" - android:textColor="?attr/colorOnSurfaceVariant" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:text="@string/anchor_suggested_radius_label" + android:textSize="12sp" + android:textColor="?attr/colorOnSurfaceVariant" /> - <TextView - android:id="@+id/tv_anchor_suggested_radius" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="2dp" - android:text="@string/anchor_suggested_radius_empty" - android:textSize="16sp" - android:textStyle="bold" - android:textColor="?attr/colorOnSurface" /> + <TextView + android:id="@+id/tv_anchor_suggested_radius" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="2dp" + android:text="@string/anchor_suggested_radius_empty" + android:textSize="16sp" + android:textStyle="bold" + android:textColor="?attr/colorOnSurface" /> - </LinearLayout> + </LinearLayout> - </com.google.android.material.card.MaterialCardView> + </LinearLayout> - <!-- Hardware Data Sources --> - <com.google.android.material.card.MaterialCardView - android:id="@+id/card_data_sources" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - app:cardCornerRadius="16dp" - app:layout_constraintTop_toBottomOf="@id/card_anchor"> + </com.google.android.material.card.MaterialCardView> - <LinearLayout + <!-- Hardware Data Sources (collapsible) --> + <com.google.android.material.card.MaterialCardView + android:id="@+id/card_data_sources" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="16dp"> + android:layout_marginBottom="16dp" + app:cardCornerRadius="16dp"> - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="HARDWARE DATA SOURCES" - android:textStyle="bold" /> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginBottom="12dp" - android:text="Enable only the hardware you have connected" - android:textSize="12sp" - android:textColor="?attr/colorOnSurfaceVariant" /> - - <!-- AIS Receiver row --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center_vertical" - android:layout_marginBottom="8dp"> + android:orientation="vertical" + android:padding="16dp"> + <!-- Collapsible header --> <LinearLayout - android:layout_width="0dp" + android:id="@+id/header_sources" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical"> + android:orientation="horizontal" + android:gravity="center_vertical" + android:clickable="true" + android:focusable="true"> <TextView - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" - android:text="AIS Receiver" - android:textSize="15sp" - android:textColor="?attr/colorOnSurface" /> + android:layout_weight="1" + android:text="HARDWARE DATA SOURCES" + android:textStyle="bold" /> <TextView + android:id="@+id/tv_sources_chevron" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Vessel tracking, collision alerts" + android:text="▶" android:textSize="12sp" android:textColor="?attr/colorOnSurfaceVariant" /> </LinearLayout> - <com.google.android.material.materialswitch.MaterialSwitch - android:id="@+id/switch_ais" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - - </LinearLayout> - - <!-- NMEA Instruments row --> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center_vertical"> - + <!-- Collapsible body — hidden by default --> <LinearLayout - android:layout_width="0dp" + android:id="@+id/body_sources" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="1" - android:orientation="vertical"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="NMEA Instruments" - android:textSize="15sp" - android:textColor="?attr/colorOnSurface" /> + android:orientation="vertical" + android:visibility="gone"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Wind, depth, boat speed" + android:layout_marginTop="4dp" + android:layout_marginBottom="12dp" + android:text="Enable only the hardware you have connected" android:textSize="12sp" android:textColor="?attr/colorOnSurfaceVariant" /> - </LinearLayout> + <!-- AIS Receiver row --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="8dp"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="AIS Receiver" + android:textSize="15sp" + android:textColor="?attr/colorOnSurface" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Vessel tracking, collision alerts" + android:textSize="12sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <com.google.android.material.materialswitch.MaterialSwitch + android:id="@+id/switch_ais" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + </LinearLayout> + + <!-- NMEA Instruments row --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="NMEA Instruments" + android:textSize="15sp" + android:textColor="?attr/colorOnSurface" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Wind, depth, boat speed" + android:textSize="12sp" + android:textColor="?attr/colorOnSurfaceVariant" /> + + </LinearLayout> + + <com.google.android.material.materialswitch.MaterialSwitch + android:id="@+id/switch_nmea" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + </LinearLayout> - <com.google.android.material.materialswitch.MaterialSwitch - android:id="@+id/switch_nmea" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + </LinearLayout> </LinearLayout> - </LinearLayout> + </com.google.android.material.card.MaterialCardView> - </com.google.android.material.card.MaterialCardView> - - <com.google.android.material.button.MaterialButton - android:id="@+id/button_plan_trip" - style="@style/Widget.Material3.Button.TonalButton" - android:layout_width="match_parent" - android:layout_height="60dp" - android:layout_marginTop="24dp" - android:text="PLAN TRIP (PRE-TRIP REPORT)" - app:layout_constraintTop_toBottomOf="@id/card_data_sources" /> - - <com.google.android.material.button.MaterialButton - android:id="@+id/button_quit" - style="@style/Widget.Material3.Button.TextButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Quit" - android:textColor="?attr/colorOnSurfaceVariant" - app:icon="@drawable/ic_close" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" /> + </LinearLayout> -</androidx.constraintlayout.widget.ConstraintLayout> +</androidx.core.widget.NestedScrollView> diff --git a/android-app/app/src/main/res/layout/fragment_voice_log.xml b/android-app/app/src/main/res/layout/fragment_voice_log.xml index 346ba81..b1c440c 100644 --- a/android-app/app/src/main/res/layout/fragment_voice_log.xml +++ b/android-app/app/src/main/res/layout/fragment_voice_log.xml @@ -189,15 +189,32 @@ android:layout_marginBottom="16dp" /> <!-- ── Trips section ── --> - <TextView + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Trips" - android:textSize="13sp" - android:textAllCaps="true" - android:letterSpacing="0.08" - android:textColor="?attr/colorOnSurfaceVariant" - android:layout_marginBottom="8dp" /> + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="8dp"> + + <TextView + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="Trips" + android:textSize="13sp" + android:textAllCaps="true" + android:letterSpacing="0.08" + android:textColor="?attr/colorOnSurfaceVariant" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/btn_plan_trip" + style="@style/Widget.Material3.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Plan Trip" + android:textSize="12sp" /> + + </LinearLayout> <!-- Empty state --> <LinearLayout |
