diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt index ba55b7d1f4..bfee71a348 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/mapview/RNMBXMapView.kt @@ -1189,7 +1189,6 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } return null; } - fun createMapView() : MapView { var created = false; mapViewImpl?.also {impl -> @@ -1199,6 +1198,10 @@ open class RNMBXMapView(private val mContext: Context, var mManager: RNMBXMapVie } } if (!created) { + val targetPath = resolveMapDataPath(mContext.filesDir) + + MapboxMapsOptions.dataPath = targetPath.absolutePath + var options: MapInitOptions? = null if (surfaceView == false) { options = MapInitOptions(context = mContext, textureView = true) @@ -1598,5 +1601,3 @@ fun OrnamentSettings.setPosAndMargins(posAndMargins: ReadableMap?) { this.margins = margins } - - diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt index 764b21f8a5..f1b8b53a11 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXOfflineModuleLegacy.kt @@ -25,8 +25,11 @@ import com.mapbox.maps.OfflineRegionStatus import com.mapbox.maps.OfflineRegionTilePyramidDefinition import com.rnmapbox.rnmbx.utils.ConvertUtils import com.rnmapbox.rnmbx.utils.extensions.toGeometryCollection +import com.rnmapbox.rnmbx.utils.newMapDataPath +import com.rnmapbox.rnmbx.utils.resolveMapDataPath import com.rnmapbox.rnmbx.utils.writableArrayOf import com.rnmapbox.rnmbx.v11compat.offlinemanager.getOfflineRegionManager +import com.mapbox.maps.MapboxMapsOptions import org.json.JSONException import org.json.JSONObject import java.io.File @@ -55,10 +58,17 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex return REACT_CLASS } - val offlineRegionManager: OfflineRegionManager by lazy { - getOfflineRegionManager { - RNMBXModule.getAccessToken(mReactContext) + @Volatile + private var offlineRegionManager: OfflineRegionManager = createOfflineRegionManager() + + private fun createOfflineRegionManager(dataPath: File? = null): OfflineRegionManager { + val targetPath = dataPath ?: resolveMapDataPath(mReactContext.filesDir) + if (!targetPath.exists()) { + targetPath.mkdirs() } + + MapboxMapsOptions.dataPath = targetPath.absolutePath + return getOfflineRegionManager { RNMBXModule.getAccessToken(mReactContext) } } private fun makeDefinition( @@ -377,9 +387,6 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex @ReactMethod fun setTileCountLimit(tileCountLimit: Int) { UiThreadUtil.runOnUiThread { - val offlineRegionManager = getOfflineRegionManager { - RNMBXModule.getAccessToken(mReactContext) - } offlineRegionManager.setOfflineMapboxTileCountLimit(tileCountLimit.toLong()) } } @@ -451,4 +458,55 @@ class RNMBXOfflineModuleLegacy(private val mReactContext: ReactApplicationContex promise.reject(mes) } } -} \ No newline at end of file + + fun reinitOfflineRegionManager(promise: Promise) { + try { + val targetPath = newMapDataPath(mReactContext.filesDir) + if (!targetPath.exists()) { + targetPath.mkdirs() + } + + UiThreadUtil.runOnUiThread { + try { + offlineRegionManager = createOfflineRegionManager(targetPath) + Log.d(LOG_TAG, "reinitOfflineRegionManager: using ${targetPath.absolutePath}") + promise.resolve(null) + } catch (t: Throwable) { + Log.e(LOG_TAG, "reinitOfflineRegionManager: failed", t) + promise.reject("reinitOfflineRegionManager error:", t) + } + } + } catch (t: Throwable) { + Log.e(LOG_TAG, "reinitOfflineRegionManager: failed to prepare path", t) + promise.reject("reinitOfflineRegionManager error:", t) + } + } + + @ReactMethod + fun hardResetDatabase(promise: Promise) { + Thread { + try { + val filesDir = mReactContext.filesDir + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customMapDataDir = File(filesDir, ".mapbox_custom") + + Log.d(LOG_TAG, "hardResetDatabase: deleting ${defaultMapDataDir.absolutePath} and ${customMapDataDir.absolutePath}") + + if (defaultMapDataDir.exists()) { + defaultMapDataDir.deleteRecursively() + } + + if (customMapDataDir.exists()) { + customMapDataDir.deleteRecursively() + } + + UiThreadUtil.runOnUiThread { + reinitOfflineRegionManager(promise) + } + } catch (t: Throwable) { + Log.e(LOG_TAG, "hardResetDatabase: failed", t) + promise.reject("hardResetDatabase error:", t) + } + }.start() + } +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt b/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt new file mode 100644 index 0000000000..a815d540ce --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/utils/MapDataPath.kt @@ -0,0 +1,26 @@ +package com.rnmapbox.rnmbx.utils + +import java.io.File +import java.util.UUID + +fun resolveMapDataPath(filesDir: File): File { + val defaultMapDataDir = File(filesDir, ".mapbox/map_data") + val customRoot = File(filesDir, ".mapbox_custom") + + if (customRoot.exists()) { + customRoot.listFiles { entry -> entry.isDirectory }?.forEach { entry -> + val candidateMapData = File(entry, "map_data") + val candidateDb = File(candidateMapData, "map_data.db") + + if (candidateDb.exists()) { + return candidateMapData + } + } + } + + return defaultMapDataDir +} + +fun newMapDataPath(filesDir: File): File { + return File(filesDir, ".mapbox_custom/${UUID.randomUUID()}/map_data") +} diff --git a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m index 36672088fd..544f0652c5 100644 --- a/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +++ b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m @@ -30,6 +30,8 @@ @interface RCT_EXTERN_MODULE(RNMBXOfflineModuleLegacy, RCTEventEmitter OfflineRe reject("migrateOfflineCache error:", error.localizedDescription, error) } } + + func reinitOfflineRegionManager( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let fileManager = FileManager.default + let targetDir: URL + + do { + targetDir = try RNMBXNewMapDataPath() + } catch { + reject("dir_error", error.localizedDescription, error) + return + } + + // Ensure directory exists + do { + try fileManager.createDirectory( + at: targetDir, + withIntermediateDirectories: true, + attributes: nil + ) + } catch { + NSLog("RNMBXOfflineModuleLegacy: failed to create custom map_data dir: \(error)") + reject("dir_error", "Failed to create custom map_data directory", error) + return + } + + DispatchQueue.main.async { + #if RNMBX_11 + MapboxMapsOptions.dataPath = targetDir + self.offlineRegionManager = OfflineRegionManager() + #else + self.offlineRegionManager = OfflineRegionManager( + resourceOptions: .init( + accessToken: RNMBXModule.accessToken ?? "", + dataPathURL: targetDir + ) + ) + #endif + + NSLog("RNMBXOfflineModuleLegacy: offlineRegionManager reinitialized with \(targetDir.path)") + resolve(nil) + } + } + + @objc + func hardResetDatabase( + _ resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) { + let fileManager = FileManager.default + let appSupport: URL + + do { + appSupport = try RNMBXApplicationSupportDirectory() + } catch { + reject("dir_error", error.localizedDescription, error) + return + } + + let defaultMapDataDir = appSupport.appendingPathComponent( + ".mapbox/map_data", + isDirectory: true + ) + let customMapDataDir = appSupport.appendingPathComponent( + ".mapbox_custom", + isDirectory: true + ) + + DispatchQueue.global(qos: .userInitiated).async { + do { + if fileManager.fileExists(atPath: defaultMapDataDir.path) { + try fileManager.removeItem(at: defaultMapDataDir) + } + + if fileManager.fileExists(atPath: customMapDataDir.path) { + try fileManager.removeItem(at: customMapDataDir) + } + } catch { + NSLog("RNMBXOfflineModuleLegacy: hardResetDatabase failed: \(error)") + DispatchQueue.main.async { + reject("hard_reset_error", "Failed to remove Mapbox offline directories", error) + } + return + } + + DispatchQueue.main.async { + self.reinitOfflineRegionManager(resolve, reject: reject) + } + } + } } diff --git a/ios/RNMBX/RNMBXMapDataPath.swift b/ios/RNMBX/RNMBXMapDataPath.swift new file mode 100644 index 0000000000..1cf4a774cf --- /dev/null +++ b/ios/RNMBX/RNMBXMapDataPath.swift @@ -0,0 +1,56 @@ +import Foundation + +private let RNMBXMapDataPathErrorDomain = "RNMBXMapDataPath" + +func RNMBXApplicationSupportDirectory() throws -> URL { + guard let appSupport = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first else { + throw NSError( + domain: RNMBXMapDataPathErrorDomain, + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Application Support directory unavailable"] + ) + } + + return appSupport +} + +func RNMBXMapDataPath() throws -> URL { + let fileManager = FileManager.default + let appSupport = try RNMBXApplicationSupportDirectory() + let defaultDataPath = appSupport.appendingPathComponent(".mapbox/map_data", isDirectory: true) + let customRoot = appSupport.appendingPathComponent(".mapbox_custom", isDirectory: true) + + guard fileManager.fileExists(atPath: customRoot.path), + let entries = try? fileManager.contentsOfDirectory( + at: customRoot, + includingPropertiesForKeys: [.isDirectoryKey], + options: [] + ) else { + return defaultDataPath + } + + for entry in entries { + if let isDir = try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory, + isDir == true { + let candidateMapData = entry.appendingPathComponent("map_data", isDirectory: true) + let candidateDb = candidateMapData.appendingPathComponent("map_data.db") + + if fileManager.fileExists(atPath: candidateDb.path) { + return candidateMapData + } + } + } + + return defaultDataPath +} + +func RNMBXNewMapDataPath() throws -> URL { + let appSupport = try RNMBXApplicationSupportDirectory() + return appSupport.appendingPathComponent( + ".mapbox_custom/\(UUID().uuidString)/map_data", + isDirectory: true + ) +} diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index 3617a9b793..cb6cfef2f3 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -254,14 +254,21 @@ open class RNMBXMapView: UIView, RCTInvalidating { if let mapViewImpl = mapViewImpl, let mapViewInstance = createAndAddMapViewImpl(mapViewImpl, self) { _mapView = mapViewInstance } else { - #if RNMBX_11 - _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) - #else + let dataPathURL = try? RNMBXMapDataPath() + let accessToken = RNMBXModule.accessToken if accessToken == nil { Logger.log(level: .error, message: "No accessToken set, please call Mapbox.setAccessToken(...)") } - let resourceOptions = ResourceOptions(accessToken: accessToken ?? "") + #if RNMBX_11 + if let dataPathURL = dataPathURL { + MapboxMapsOptions.dataPath = dataPathURL + } + _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) + #else + let resourceOptions = dataPathURL.map { + ResourceOptions(accessToken: accessToken ?? "", dataPathURL: $0) + } ?? ResourceOptions(accessToken: accessToken ?? "") _mapView = MapView(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) #endif _mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -1923,4 +1930,3 @@ class RNMBXPointAnnotationManager : AnnotationInteractionDelegate { manager.refresh() } } - diff --git a/src/modules/offline/offlineManagerLegacy.ts b/src/modules/offline/offlineManagerLegacy.ts index 523071f673..95a0717625 100644 --- a/src/modules/offline/offlineManagerLegacy.ts +++ b/src/modules/offline/offlineManagerLegacy.ts @@ -130,6 +130,20 @@ class OfflineManagerLegacy { await this._initialize(true); } + /** + * Fully resets offline region database file by deleting both `.mapbox/map_data` and `.mapbox_custom` directories, + * then reinitializes the OfflineRegionManager to point to a new data directory. + * + * Reinitialization is required because the Mapbox SDK does not automatically recreate the `map_data` database after deletion. + * After calling this, all subsequent offline region operations will use a fresh, isolated storage location. + * Unmount active MapView instances before calling this; SDK data path is fixed at MapView creation. + */ + async hardResetDatabase(): Promise { + await MapboxOfflineManager.hardResetDatabase(); + this._offlinePacks = {}; + await this._initialize(true); + } + /** * Retrieves all the current offline packs that are stored in the database. *