Map corruption recovery

An onboard NDS map or its associated keystore file can occasionally become corrupted. This could be due to a variety of reasons, namely a storage failure, an interrupted write, or an unexpected device shutdown. When this happens, the SDK detects the problem and notifies your application so you can recover without crashing.

This guide explains what failures can occur, how to listen for them, and how to replace the map or keystore to restore normal operation.

Understanding recoverable failures

When the SDK detects a problem with the onboard map or keystore it emits one of the following recoverable failures as a TomTomSdkFailure, each carrying the RegionStore reference you use for recovery.

Failure (TomTomSdkFailure)MeaningRecovery action

CannotReadMap

The map directory is not accessible, the ROOT.NDS file is missing, or an error occurred while reading map data.

Call replaceMap() with a valid map. Before doing so, verify that your keystore and password are correct — a wrong keystore can produce this failure even on a valid map.

CannotReadKeyStore

The keystore file is missing or corrupt.

Call replaceKeystore() with a valid keystore file.

A wrong keystore or password will cause a CannotReadMap failure to be reported even when the map itself is intact. Always verify your keystore configuration before replacing the map.

Failures can be reported both during initialization and at runtime while the store is in use.

Listening for failures

Collect from the TomTomSdk.failures flow to be notified when the SDK detects a problem:

lifecycleScope.launch {
TomTomSdk.failures.collect { failure ->
when (failure) {
is TomTomSdkFailure.CannotReadMap -> recoverMap(failure.regionStore)
is TomTomSdkFailure.CannotReadKeyStore -> recoverKeystore(failure.regionStore)
}
}
}

The recoverMap and recoverKeystore helper functions are implemented in the sections below.

Replacing the map

Call replaceMap() to overwrite the current onboard map with a known-good map. The operation runs on a background thread and reports progress and completion through its callback.

Expected directory structure

The newMapDirectoryPath parameter must point to the DATA directory of the backup map, the directory that directly contains the map’s database files, including ROOT.NDS. The typical on-disk layout of a backup map looks like this:

backup-map/
└── DATA/ <-- pass this path as newMapDirectoryPath
├── ROOT.NDS
└── ...

Preparing the backup map from app assets

A common pattern is to bundle a known-good (empty) map as a zip file in your app’s assets and extract it to internal storage on first launch. The following helpers extract the zip to internal storage and return the DATA directory path ready to pass to replaceMap():

private fun extractBackupMap(): File {
val outputDir = File(context.filesDir, "backup-map")
if (!outputDir.exists()) {
extractZipToDir("backup-map.zip", outputDir)
}
return File(outputDir, "DATA")
}
private fun extractZipToDir(
assetName: String,
outputDir: File,
) {
context.assets.open(assetName).use { input ->
ZipInputStream(input).use { zip ->
generateSequence { zip.nextEntry }.forEach { entry ->
extractZipEntry(zip, entry, outputDir)
}
}
}
}
private fun extractZipEntry(
zip: ZipInputStream,
entry: ZipEntry,
outputDir: File,
) {
val outFile = File(outputDir, entry.name)
if (entry.isDirectory) {
outFile.mkdirs()
} else {
outFile.parentFile?.mkdirs()
outFile.outputStream().use { zip.copyTo(it) }
}
}

Calling replaceMap

@Suppress("UNUSED_ANONYMOUS_PARAMETER")
private fun recoverMap(regionStore: RegionStore) {
val backupDataDir = extractBackupMap() // points to the DATA directory
regionStore.replaceMap(
newMapDirectoryPath = backupDataDir,
newMapFileList = null, // null copies all files without checksum validation
callback = object : Callback<Unit, MapUpdateError> {
override fun onSuccess(result: Unit) {
// Map replaced successfully; normal operation resumes.
}
override fun onFailure(failure: MapUpdateError) {
// Replacement failed. The store has no valid map.
// Call replaceMap() again with correct parameters to recover.
}
},
progressReporter = { progress ->
// progress is 0–100
},
)
}

The newMapFileList parameter accepts a list of NewMapFile entries with per-file checksums. When provided, each file is validated before being written. If any file fails validation, MapUpdateError.FileValidationError is reported and no partial map is left behind.

The replacement deletes the existing map directory before copying the new files. If an error occurs mid-copy, there will be no valid onboard map until replaceMap() succeeds.

Once onSuccess is called, the backup map files extracted to internal storage are no longer needed. Delete them at that point to free up device storage.

Replacing the keystore

Call replaceKeystore() to overwrite the existing keystore file with a valid one. The new file must be readable from the path you provide.

private fun recoverKeystore(regionStore: RegionStore) {
regionStore.replaceKeystore(
newKeystoreFilePath = File("/path/to/backup/keystore.sqlite"),
keystoreChecksum = "", // pass a non-empty string to validate before writing
callback = object : Callback<Unit, MapUpdateError> {
override fun onSuccess(result: Unit) {
// Keystore replaced successfully.
}
override fun onFailure(failure: MapUpdateError) {
// Replacement failed. Check that the source file exists and is readable.
}
},
)
}

Pass a non-empty keystoreChecksum to validate the replacement file before it is written. If the checksum does not match, MapUpdateError.FileValidationError is reported and the existing keystore is left unchanged.

Concurrent recovery calls

Only one recovery operation, either replaceMap() or replaceKeystore(), can be in progress at a time. If you call either method while a recovery is already running, the callback immediately receives MapUpdateError indicating the operation is already in progress. Wait for the in-progress operation to complete before retrying.

If a new error is detected while a recovery is already in progress — for example, a keystore error occurring during a map replacement — the SDK defers it and delivers it once the current recovery finishes, through the same channel you receive failures on. Your handler must therefore be prepared to be called again immediately after a recovery completes, potentially with a different failure type requiring a second recovery step.

Enabling updates before recovery

Map updates must be enabled on the RegionStore before calling replaceMap() or replaceKeystore(). If updates are disabled at the time of the call, an error is reported. Enable updates before initiating recovery:

regionStore.setUpdatesEnabled(true)
When using the Extended flavor

The Extended flavor of Maps and Navigation SDK for Android is only available upon request. Contact Sales to get started.

If you construct the store directly with NdsStore — an integration pattern available only in the Extended flavor — failures are delivered to an NdsStoreFailureListener passed at construction, instead of the TomTomSdk.failures flow. When a failureListener is provided, initialization succeeds even if the map or keystore is missing or corrupt and errors are delivered through the listener; if you pass null, initialization throws instead.

The listener receives the NdsStoreRecoverableFailure hierarchy: MissingMap and CannotReadMap — both recovered with replaceMap(), matching the CannotReadMap case above — and CannotReadKeyStore. A failure deferred during an ongoing recovery is delivered to this listener once the recovery finishes.

val failureListener = object : NdsStoreFailureListener {
override fun onFailure(
ndsStore: NdsStore,
ndsStoreRecoverableFailure: NdsStoreRecoverableFailure,
) {
when (ndsStoreRecoverableFailure) {
is NdsStoreRecoverableFailure.MissingMap,
is NdsStoreRecoverableFailure.CannotReadMap,
-> recoverMap(ndsStore)
is NdsStoreRecoverableFailure.CannotReadKeyStore -> recoverKeystore(ndsStore)
}
}
}
ndsStore = NdsStore(
context = context,
ndsStorePath = File("/path/to/nds/map"),
apiKey = "YOUR_API_KEY",
failureListener = failureListener,
)

The recoverMap and recoverKeystore functions referenced in the listener are the ones documented above.