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) | Meaning | Recovery action |
| The map directory is not accessible, the | Call |
| The keystore file is missing or corrupt. | Call |
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.
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
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.