summaryrefslogtreecommitdiff
path: root/android-app
diff options
context:
space:
mode:
Diffstat (limited to 'android-app')
-rw-r--r--android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt86
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt19
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/track/TrackRepository.kt6
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt5
4 files changed, 49 insertions, 67 deletions
diff --git a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt
index a13ef7f..fecd9cc 100644
--- a/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt
+++ b/android-app/app/src/androidTest/kotlin/org/terst/nav/MainActivitySmokeTest.kt
@@ -1,6 +1,5 @@
package org.terst.nav
-import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -8,21 +7,26 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Smoke tests: verify the main UI surfaces launch and respond correctly.
- * These run on an emulator without GPS permission, so no LocationService.
+ * Run without GPS permission — LocationService is not started.
*
- * Run locally: ./gradlew connectedDebugAndroidTest
- * In CI: smoke-test job via android-emulator-runner
+ * Locally: ./gradlew connectedDebugAndroidTest
+ * CI: smoke-test job via android-emulator-runner
*/
@RunWith(AndroidJUnit4::class)
class MainActivitySmokeTest {
+ @get:Rule
+ val activityRule = ActivityScenarioRule(MainActivity::class.java)
+
@Before
fun setup() {
NavApplication.isTesting = true
@@ -32,10 +36,8 @@ class MainActivitySmokeTest {
@Test
fun mainActivity_launches_withoutCrash() {
- ActivityScenario.launch(MainActivity::class.java).use { scenario ->
- scenario.onActivity { activity ->
- assert(!activity.isFinishing) { "MainActivity finished immediately after launch" }
- }
+ activityRule.scenario.onActivity { activity ->
+ assert(!activity.isFinishing) { "MainActivity finished immediately after launch" }
}
}
@@ -43,89 +45,69 @@ class MainActivitySmokeTest {
@Test
fun bottomNav_allFourTabs_areDisplayed() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Map")).check(matches(isDisplayed()))
- onView(withText("Instruments")).check(matches(isDisplayed()))
- onView(withText("Log")).check(matches(isDisplayed()))
- onView(withText("Safety")).check(matches(isDisplayed()))
- }
+ onView(withText("Map")).check(matches(isDisplayed()))
+ onView(withText("Instruments")).check(matches(isDisplayed()))
+ onView(withText("Log")).check(matches(isDisplayed()))
+ onView(withText("Safety")).check(matches(isDisplayed()))
}
@Test
fun bottomNav_safetyTab_showsSafetyDashboard() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Safety")).perform(click())
- onView(withText("Safety Dashboard")).check(matches(isDisplayed()))
- onView(withText("ACTIVATE MOB")).check(matches(isDisplayed()))
- onView(withText("ANCHOR WATCH")).check(matches(isDisplayed()))
- }
+ onView(withText("Safety")).perform(click())
+ onView(withText("Safety Dashboard")).check(matches(isDisplayed()))
+ onView(withText("ACTIVATE MOB")).check(matches(isDisplayed()))
+ onView(withText("ANCHOR WATCH")).check(matches(isDisplayed()))
}
@Test
fun bottomNav_logTab_showsVoiceLogUi() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Log")).perform(click())
- onView(withContentDescription("Start voice recognition")).check(matches(isDisplayed()))
- }
+ onView(withText("Log")).perform(click())
+ onView(withContentDescription("Start voice recognition")).check(matches(isDisplayed()))
}
@Test
fun bottomNav_instrumentsTab_isSelectable() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Instruments")).perform(click())
- onView(withId(R.id.instrument_bottom_sheet)).check(matches(isDisplayed()))
- }
+ onView(withText("Instruments")).perform(click())
+ onView(withId(R.id.instrument_bottom_sheet)).check(matches(isDisplayed()))
}
@Test
fun bottomNav_mapTab_returnsFromOverlay() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Safety")).perform(click())
- onView(withText("Map")).perform(click())
- onView(withId(R.id.mapView)).check(matches(isDisplayed()))
- }
+ onView(withText("Safety")).perform(click())
+ onView(withText("Map")).perform(click())
+ onView(withId(R.id.mapView)).check(matches(isDisplayed()))
}
// ── Persistent FABs ────────────────────────────────────────────────────
@Test
fun fabMob_isAlwaysVisible() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withContentDescription("Man Overboard")).check(matches(isDisplayed()))
- }
+ onView(withContentDescription("Man Overboard")).check(matches(isDisplayed()))
}
@Test
fun fabMob_remainsVisibleOnSafetyTab() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withText("Safety")).perform(click())
- onView(withContentDescription("Man Overboard")).check(matches(isDisplayed()))
- }
+ onView(withText("Safety")).perform(click())
+ onView(withContentDescription("Man Overboard")).check(matches(isDisplayed()))
}
// ── Track recording ────────────────────────────────────────────────────
@Test
fun fabRecordTrack_isDisplayedWithRecordDescription() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withContentDescription("Record Track")).check(matches(isDisplayed()))
- }
+ onView(withContentDescription("Record Track")).check(matches(isDisplayed()))
}
@Test
fun fabRecordTrack_togglesToStopRecording_onFirstClick() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withContentDescription("Record Track")).perform(click())
- onView(withContentDescription("Stop Recording")).check(matches(isDisplayed()))
- }
+ onView(withContentDescription("Record Track")).perform(click())
+ onView(withContentDescription("Stop Recording")).check(matches(isDisplayed()))
}
@Test
fun fabRecordTrack_togglesBackToRecord_onSecondClick() {
- ActivityScenario.launch(MainActivity::class.java).use {
- onView(withContentDescription("Record Track")).perform(click())
- onView(withContentDescription("Stop Recording")).perform(click())
- onView(withContentDescription("Record Track")).check(matches(isDisplayed()))
- }
+ onView(withContentDescription("Record Track")).perform(click())
+ onView(withContentDescription("Stop Recording")).perform(click())
+ onView(withContentDescription("Record Track")).check(matches(isDisplayed()))
}
}
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 f887a43..f9d4dbd 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
@@ -22,7 +22,10 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -45,7 +48,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private var instrumentHandler: InstrumentHandler? = null
private var mapHandler: MapHandler? = null
private var anchorWatchHandler: AnchorWatchHandler? = null
- private var loadedStyle: Style? = null
+ private val loadedStyleFlow = MutableStateFlow<Style?>(null)
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
private lateinit var fragmentContainer: FrameLayout
@@ -147,17 +150,13 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
private fun hideOverlays() {
fragmentContainer.visibility = View.GONE
- // Clear backstack if needed
}
override fun onActivateMob() {
lifecycleScope.launch {
LocationService.locationFlow.firstOrNull()?.let { gpsData ->
val mediaPlayer = MediaPlayer.create(this@MainActivity, R.raw.mob_alarm)
- // In a real redesign, we'd show a specialized MOB fragment
- // For now, keep existing handler logic but maybe toggle visibility
mobHandler?.activateMob(gpsData.latitude, gpsData.longitude, mediaPlayer)
- // Ensure MOB UI is visible - we might need to add it back to activity_main if removed
}
}
}
@@ -167,7 +166,6 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
private fun setupHandlers() {
- // ... (Keep existing handler initialization, just update view IDs as needed)
instrumentHandler = InstrumentHandler(
valueAws = findViewById(R.id.value_aws),
valueTws = findViewById(R.id.value_tws),
@@ -243,7 +241,7 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
.withLayer(RasterLayer("openseamap-layer", "openseamap-source"))
maplibreMap.setStyle(style) { style ->
- loadedStyle = style
+ loadedStyleFlow.value = style
val anchorBitmap = rasterizeDrawable(R.drawable.ic_anchor)
val arrowBitmap = rasterizeDrawable(R.drawable.ic_tidal_arrow)
mapHandler?.setupLayers(style, anchorBitmap, arrowBitmap)
@@ -265,10 +263,9 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
}
}
lifecycleScope.launch {
- viewModel.trackPoints.collect { points ->
- val style = loadedStyle ?: return@collect
- mapHandler?.updateTrackLayer(style, points)
- }
+ loadedStyleFlow.filterNotNull()
+ .combine(viewModel.trackPoints) { style, points -> style to points }
+ .collect { (style, points) -> mapHandler?.updateTrackLayer(style, points) }
}
}
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 c90adb9..7953822 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
@@ -16,8 +16,10 @@ class TrackRepository {
isRecording = false
}
- fun addPoint(point: TrackPoint) {
- if (isRecording) points.add(point)
+ fun addPoint(point: TrackPoint): Boolean {
+ if (!isRecording) return false
+ points.add(point)
+ return true
}
fun getPoints(): List<TrackPoint> = points.toList()
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
index 33decbe..0efff52 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/MainViewModel.kt
@@ -71,8 +71,9 @@ class MainViewModel(
windSpeedKnots = 0.0, windAngleDeg = 0.0, isTrueWind = false,
timestampMs = System.currentTimeMillis()
)
- trackRepository.addPoint(point)
- _trackPoints.value = trackRepository.getPoints()
+ if (trackRepository.addPoint(point)) {
+ _trackPoints.value = trackRepository.getPoints()
+ }
}
private val aisHubApi: AisHubApiService by lazy {