Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import org.scottishtecharmy.soundscape.i18n.ComposeLocalizedStrings
import org.scottishtecharmy.soundscape.network.DownloadStateCommon
import org.scottishtecharmy.soundscape.screens.home.offlinemaps.NearbyExtractsState
import java.io.File
import java.io.FileOutputStream

private fun DownloadState.toCommon(): DownloadStateCommon = when (this) {
is DownloadState.Idle -> DownloadStateCommon.Idle
Expand Down Expand Up @@ -109,6 +108,7 @@ class AndroidOfflineMapsManager(

private fun refreshDownloaded() {
val dir = extractsDir()
deleteOrphanedSidecars(dir)
_downloadedExtractsFc.value = findExtracts(dir.path) ?: FeatureCollection()
}

Expand Down Expand Up @@ -153,23 +153,24 @@ class AndroidOfflineMapsManager(
// version's files once this one is published (see logicalBaseName below).
val versionedFilename = "$logicalBase.v${System.currentTimeMillis()}.pmtiles"
val path = "${extractsDir().path}/$versionedFilename"
try {
val moshi = GeoMoshi.registerAdapters(Moshi.Builder()).build()
val adapter = moshi.adapter(Feature::class.java)
FileOutputStream("$path.geojson").use {
it.write(
adapter.toJson(current).toByteArray()
)
}
// The metadata sidecar is written by the downloader once the extract is saved, not here
// before it starts: a download that failed or was cancelled used to leave its sidecar
// behind, for an extract that was never there.
val metadataJson = try {
GeoMoshi.registerAdapters(Moshi.Builder()).build()
.adapter(Feature::class.java)
.toJson(current)
} catch (e: Exception) {
Log.e(TAG, "Failed to write extract metadata", e)
Log.e(TAG, "Failed to encode extract metadata", e)
null
}
val extractSize = (current.properties?.get("extract-size") as? Number)?.toDouble()
downloader.startDownload(
"${BuildConfig.EXTRACT_PROVIDER_URL}$filename",
path,
extractSize,
logicalBaseName = logicalBase,
metadataJson = metadataJson,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,16 @@ class OfflineDownloader(injectedFileDownloader: FileDownloader? = null) {
* directory whose name starts with "<logicalBaseName>." is deleted - this is what actually
* retires a previous version of the same extract (see the comment above the rename below for
* why in-place replacement isn't safe for pmtiles extracts that MapLibre may have opened).
* @param metadataJson If set, written to the "<outputFilePath>.geojson" sidecar once the
* download is published - and only then, so that a download which fails or is cancelled
* doesn't leave a sidecar behind for an extract that was never saved.
*/
fun startDownload(
fileUrl: String,
outputFilePath: String,
extractSize: Double?,
logicalBaseName: String? = null,
metadataJson: String? = null,
) {
if (downloadJob?.isActive == true) {
Log.w(TAG, "Download is already in progress.")
Expand Down Expand Up @@ -162,6 +166,13 @@ class OfflineDownloader(injectedFileDownloader: FileDownloader? = null) {
// are always in the same directory.
if (tempFile.renameTo(finalFile)) {
Log.i(TAG, "Download successful. File saved to: ${finalFile.path}")
if (metadataJson != null) {
try {
File("$outputFilePath.geojson").writeText(metadataJson)
} catch (e: Exception) {
Log.e(TAG, "Failed to write extract metadata", e)
}
}
// Clean up old versions before publishing Success - an observer
// reacting to Success (e.g. refreshing the extract list) should
// never see a superseded version still on disk.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ fun getMetadata(pmtilesPath: String): Feature? {
return null
}

/**
* Delete the .geojson metadata sidecars in [dir] which have no extract next to them. Downloads
* used to write the sidecar before they started, and left it behind when they failed or were
* cancelled; those orphans are never listed, but they stay on the phone forever.
*/
fun deleteOrphanedSidecars(dir: File) {
dir.listFiles { file -> file.name.endsWith(".pmtiles.geojson") }?.forEach { sidecar ->
if (!File(dir, sidecar.name.removeSuffix(".geojson")).exists()) {
sidecar.delete()
}
}
}

fun findExtracts(path: String): FeatureCollection? {
// Find any extracts that we have downloaded
val extractsDir = File(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.junit.Test
import org.scottishtecharmy.soundscape.network.createFileDownloader
import org.scottishtecharmy.soundscape.utils.DownloadState
import org.scottishtecharmy.soundscape.utils.OfflineDownloader
import org.scottishtecharmy.soundscape.utils.deleteOrphanedSidecars
import java.io.File

/**
Expand Down Expand Up @@ -114,12 +115,11 @@ class OfflineDownloaderTest {
val unrelatedExtract = File(tempDir, "glasgow-gbx.bin").apply { writeText("unrelated") }

val newOutputFile = File(tempDir, "glasgow-gb.v2000.bin")
// AndroidOfflineMapsManager.startDownload() writes this sidecar *before* calling
// startDownload - it must survive the post-publish cleanup, which previously deleted
// it (it starts with the logical prefix and its name isn't *exactly*
// newOutputFile.name), silently hiding every freshly downloaded extract from the
// offline-maps UI.
val newSidecar = File(tempDir, "glasgow-gb.v2000.bin.geojson").apply { writeText("{}") }
// The download writes its own sidecar once published, and it must survive the
// post-publish cleanup, which once deleted it (it starts with the logical prefix and
// its name isn't *exactly* newOutputFile.name), silently hiding every freshly
// downloaded extract from the offline-maps UI.
val newSidecar = File(tempDir, "glasgow-gb.v2000.bin.geojson")

val fake = FakeResponses(listOf({ body(1000) }))
val downloader = OfflineDownloader(fakeFileDownloader(fake))
Expand All @@ -128,6 +128,7 @@ class OfflineDownloaderTest {
newOutputFile.path,
extractSize = 1000.0,
logicalBaseName = "glasgow-gb",
metadataJson = "{}",
)

val state = runBlocking {
Expand Down Expand Up @@ -164,14 +165,15 @@ class OfflineDownloaderTest {
val unrelatedExtract = File(tempDir, "glasgow-gbx.bin").apply { writeText("unrelated") }

val newOutputFile = File(tempDir, "glasgow-gb.v2000.bin")
val newSidecar = File(tempDir, "glasgow-gb.v2000.bin.geojson").apply { writeText("{}") }
val newSidecar = File(tempDir, "glasgow-gb.v2000.bin.geojson")
val fake = FakeResponses(listOf({ body(1000) }))
val downloader = OfflineDownloader(fakeFileDownloader(fake))
downloader.startDownload(
"https://example.test/extract",
newOutputFile.path,
extractSize = 1000.0,
logicalBaseName = "glasgow-gb",
metadataJson = "{}",
)

val state = runBlocking {
Expand Down Expand Up @@ -256,4 +258,60 @@ class OfflineDownloaderTest {
file.parentFile?.deleteRecursively()
}
}

/**
* A download that fails must leave nothing behind - its metadata sidecar used to be written
* before the download started, and stayed on the phone forever with no extract next to it.
*/
@Test
fun failedDownloadLeavesNoSidecar() {
val tempDir = File(System.getProperty("java.io.tmpdir"), "offline-dl-${System.nanoTime()}")
tempDir.mkdirs()
try {
val outputFile = File(tempDir, "helsinki-fi.v2000.bin")
val fake = FakeResponses(listOf({ truncatedBody(advertised = 1000L, actual = 500) }))
val downloader = OfflineDownloader(fakeFileDownloader(fake))
downloader.startDownload(
"https://example.test/extract",
outputFile.path,
extractSize = 1000.0,
logicalBaseName = "helsinki-fi",
metadataJson = "{}",
)

val state = runBlocking {
withTimeout(10_000) {
downloader.downloadState.first {
it is DownloadState.Success || it is DownloadState.Error
}
}
}

assertTrue("expected Error but was $state", state is DownloadState.Error)
assertTrue("no sidecar for a failed download", tempDir.listFiles().isNullOrEmpty())
} finally {
tempDir.deleteRecursively()
}
}

@Test
fun orphanedSidecarsAreDeleted() {
val tempDir = File(System.getProperty("java.io.tmpdir"), "offline-dl-${System.nanoTime()}")
tempDir.mkdirs()
try {
val extract = File(tempDir, "glasgow-gb.v1000.pmtiles").apply { writeText("x") }
val sidecar = File(tempDir, "glasgow-gb.v1000.pmtiles.geojson").apply { writeText("{}") }
val orphan = File(tempDir, "helsinki-fi.v2000.pmtiles.geojson").apply { writeText("{}") }
val download = File(tempDir, "budapest-hu.v3000.pmtiles.downloading").apply { writeText("x") }

deleteOrphanedSidecars(tempDir)

assertTrue(extract.exists())
assertTrue("an extract's own sidecar must survive", sidecar.exists())
assertTrue("a sidecar with no extract should be deleted", !orphan.exists())
assertTrue("a download in progress isn't touched", download.exists())
} finally {
tempDir.deleteRecursively()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,10 @@ class SearchTest {
@Test
fun categorySearchNamesAnUnnamedPlaceByItsType() {
runBlocking {
// The toilets by Milngavie station (OSM way 218224201) are an unnamed building. They
// used to be listed as "Milngavie" - their address - with nothing to say they were
// toilets, and twice, because the building crosses a tile boundary.
// The toilets near the James Gale Memorial in Milngavie (OSM way 218224201) are an
// unnamed building. They used to be listed as "Milngavie" - their address - with
// nothing to say they were toilets, and twice, because the building crosses a tile
// boundary.
val currentLocation = LngLatAlt(-4.3092376, 55.9497288)
val gridState = getGridStateForLocation(currentLocation, MAX_ZOOM_LEVEL, GRID_SIZE)
val settlementState = getGridStateForLocation(currentLocation, 12, 3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ actual fun PlatformMapContainer(
extractGeometry: Geometry?,
forceOnlineTiles: Boolean,
onInteractionChanged: (Boolean) -> Unit,
onBeaconLocationEdited: ((LngLatAlt) -> Unit)?,
) {
val context = LocalContext.current

Expand Down Expand Up @@ -109,5 +110,6 @@ actual fun PlatformMapContainer(
baseStyle = baseStyle,
extractGeometry = extractGeometry,
onInteractionChanged = onInteractionChanged,
onBeaconLocationEdited = onBeaconLocationEdited,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,6 @@
<string name="location_detail_full_screen_hint">عرض الخريطة بملء الشاشة</string>
<string name="location_detail_exit_full_screen_hint">الخروج من الخريطة بملء الشاشة</string>
<string name="location_detail_full_screen_for_edit_hint">عرض الخريطة بملء الشاشة وتعديل موقع العلامة</string>
<string name="location_detail_exit_full_screen_for_edit_hint">الخروج من وضع تعديل موقع العلامة</string>
<string name="route_detail_action_start_route">بدء المسار</string>
<string name="route_detail_action_stop_route">إيقاف المسار</string>
<string name="route_detail_action_edit">تعديل المسار</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
<string name="location_detail_full_screen_hint">Показване на картата на цял екран</string>
<string name="location_detail_exit_full_screen_hint">излезете от картата на цял екран</string>
<string name="location_detail_full_screen_for_edit_hint">Показване на картата на цял екран за редактиране на местоположението на маркера</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Изход от режима за редактиране на местоположението на маркера</string>
<string name="route_detail_action_start_route">Стартирай маршрута</string>
<string name="route_detail_action_stop_route">Спри маршрута</string>
<string name="route_detail_action_edit">Редактирай маршрута</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,6 @@
<string name="location_detail_full_screen_hint">মানচিত্রটি পূর্ণ পর্দা করুন</string>
<string name="location_detail_exit_full_screen_hint">পূর্ণ পর্দার মানচিত্র থেকে বের হতে</string>
<string name="location_detail_full_screen_for_edit_hint">মানচিত্রটি পূর্ণ পর্দা করুন এবং মার্কারের অবস্থান সম্পাদনা করুন</string>
<string name="location_detail_exit_full_screen_for_edit_hint">মার্কার অবস্থান সম্পাদনা মোড থেকে প্রস্থান করুন</string>
<string name="route_detail_action_start_route">রুট শুরু করুন</string>
<string name="route_detail_action_stop_route">রুট বন্ধ করুন</string>
<string name="route_detail_action_edit">রুট সম্পাদনা করুন</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
<string name="location_detail_full_screen_hint">Mostra el mapa a pantalla completa</string>
<string name="location_detail_exit_full_screen_hint">sortir del mapa a pantalla completa</string>
<string name="location_detail_full_screen_for_edit_hint">Mostra el mapa a pantalla completa i edita la ubicació del marcador</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Surt del mode d'edició de la ubicació del marcador</string>
<string name="route_detail_action_start_route">Inicia la ruta</string>
<string name="route_detail_action_stop_route">Atura la ruta</string>
<string name="route_detail_action_edit">Edita la ruta</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,6 @@
<string name="location_detail_full_screen_hint">Zobrazit mapu na celou obrazovku</string>
<string name="location_detail_exit_full_screen_hint">ukončit zobrazení mapy na celou obrazovku</string>
<string name="location_detail_full_screen_for_edit_hint">Zobrazit mapu na celou obrazovku a upravit polohu značky</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Ukončit režim úpravy polohy značky</string>
<string name="route_detail_action_start_route">Spustit trasu</string>
<string name="route_detail_action_stop_route">Zastavit trasu</string>
<string name="route_detail_action_edit">Upravit trasu</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,6 @@
<string name="tour_continue_hint">lukke popuppen og fortsætte tutorialen</string>
<string name="location_detail_action_beacon_from_markers">Start lydfyr ved dette mærke</string>
<string name="location_detail_full_screen_for_edit_hint">Vis kortet i fuld skærm, og rediger mærkets placering</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Afslut redigering af mærkets placering</string>
<string name="street_description_relative_before">På %1$s lige før %2$s</string>
<string name="street_description_relative_after">På %1$s lige efter %2$s</string>
<string name="street_description_intersection">Nær krydset ved %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,6 @@
<string name="tour_continue_hint">das Popup schließen und das Tutorial fortsetzen</string>
<string name="location_detail_action_beacon_from_markers">Audiobeacon an dieser Markierung starten</string>
<string name="location_detail_full_screen_for_edit_hint">Karte im Vollbild anzeigen und den Markierungsstandort bearbeiten</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Bearbeitungsmodus für Markierungsstandort beenden</string>
<string name="street_description_relative_before">Auf %1$s, kurz vor %2$s</string>
<string name="street_description_relative_after">Auf %1$s, kurz nach %2$s</string>
<string name="street_description_intersection">In der Nähe der Kreuzung von %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,6 @@
<string name="tour_continue_hint">κλείσετε το αναδυόμενο παράθυρο και να συνεχίσετε τον οδηγό</string>
<string name="location_detail_action_beacon_from_markers">Έναρξη ηχητικού σήματος σε αυτόν τον δείκτη</string>
<string name="location_detail_full_screen_for_edit_hint">Μεγιστοποίηση του χάρτη σε πλήρη οθόνη και επεξεργασία της θέσης του δείκτη</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Έξοδος από τη λειτουργία επεξεργασίας της θέσης του δείκτη</string>
<string name="street_description_relative_before">Στην %1$s, λίγο πριν από %2$s</string>
<string name="street_description_relative_after">Στην %1$s, λίγο μετά από %2$s</string>
<string name="street_description_intersection">Κοντά στη διασταύρωση %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,6 @@
<string name="tour_continue_hint">close the popup and continue the tutorial</string>
<string name="location_detail_action_beacon_from_markers">Start audio beacon at this marker</string>
<string name="location_detail_full_screen_for_edit_hint">Make the map full screen and edit the marker location</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Exit marker location editing mode</string>
<string name="street_description_relative_before">On %1$s, just before %2$s</string>
<string name="street_description_relative_after">On %1$s, just after %2$s</string>
<string name="street_description_intersection">Near the intersection of %1$s</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,6 @@
<string name="tour_continue_hint">cerrar la ventana emergente y continuar el tutorial</string>
<string name="location_detail_action_beacon_from_markers">Iniciar la señal de audio en este marcador</string>
<string name="location_detail_full_screen_for_edit_hint">Poner el mapa en pantalla completa y editar la ubicación del marcador</string>
<string name="location_detail_exit_full_screen_for_edit_hint">Salir del modo de edición de la ubicación del marcador</string>
<string name="street_description_relative_before">{es:En %1$s}, justo antes {es:de %2$s}</string>
<string name="street_description_relative_after">{es:En %1$s}, justo después {es:de %2$s}</string>
<string name="street_description_intersection">Cerca de la intersección de %1$s</string>
Expand Down
Loading
Loading