package expo.modules.gaodemap.map import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import android.os.StatFs import android.os.Environment import android.util.Log import com.amap.api.maps.offlinemap.OfflineMapActivity import com.amap.api.maps.offlinemap.OfflineMapCity import com.amap.api.maps.offlinemap.OfflineMapManager import com.amap.api.maps.offlinemap.OfflineMapProvince import com.amap.api.maps.offlinemap.OfflineMapStatus import expo.modules.gaodemap.map.modules.SDKInitializer import expo.modules.kotlin.exception.CodedException import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition /** * 高德地图离线地图模块 (Android) */ class ExpoGaodeMapOfflineModule : Module() { private var offlineMapManager: OfflineMapManager? = null private val downloadingCities = mutableSetOf() private val pausedCities = mutableSetOf() // 线程安全锁 private val lock = Any() /** * 兼容获取 START_DOWNLOAD_FAILED 状态码 * 国内版拼写为 START_DOWNLOAD_FAILD,Google Play 版修正为 START_DOWNLOAD_FAILED */ private val startDownloadFailedCode: Int by lazy { try { OfflineMapStatus::class.java.getField("START_DOWNLOAD_FAILED").getInt(null) } catch (e: Exception) { try { OfflineMapStatus::class.java.getField("START_DOWNLOAD_FAILD").getInt(null) } catch (e2: Exception) { -1 } } } /** * 兼容获取下载进度 * Google Play 版本 SDK 可能修复了 getcompleteCode 的命名或使用了不同的 API */ private fun getDownloadProgress(obj: Any): Int { try { // 尝试标准版的命名 (getcompleteCode) val method = obj.javaClass.getMethod("getcompleteCode") return method.invoke(obj) as Int } catch (e: Exception) { try { // 尝试修正后的命名 (getCompleteCode) - Google Play 版本可能使用此命名 val method = obj.javaClass.getMethod("getCompleteCode") return method.invoke(obj) as Int } catch (e2: Exception) { // 如果都失败了,尝试直接访问 completeCode 字段 try { val field = obj.javaClass.getField("completeCode") return field.getInt(obj) } catch (e3: Exception) { return 0 } } } } override fun definition() = ModuleDefinition { Name("ExpoGaodeMapOffline") // ==================== 事件定义 ==================== Events( "onDownloadProgress", "onDownloadComplete", "onDownloadError", "onUnzipProgress", "onDownloadPaused", "onDownloadCancelled" ) // ==================== 模块生命周期 ==================== OnCreate { // 避免在应用启动早期(JS 隐私同意前)触发 OfflineMapManager 初始化导致 555571 getOfflineMapManager() } OnDestroy { offlineMapManager?.destroy() offlineMapManager = null downloadingCities.clear() } AsyncFunction("openOfflineMapUI") { openOfflineMapUI() } // ==================== 地图列表管理 ==================== AsyncFunction("getAvailableCities") { val manager = getOfflineMapManager() val cities = manager?.offlineMapCityList ?: emptyList() val downloadedCityKeys = getDownloadedCityKeys(manager) cities.map { city -> convertCityToMap(city, downloadedCityKeys) } } AsyncFunction("getAvailableProvinces") { val provinces = getOfflineMapManager()?.offlineMapProvinceList ?: emptyList() provinces.map { province -> convertProvinceToMap(province) } } AsyncFunction("getCitiesByProvince") { provinceCode: String -> val manager = getOfflineMapManager() val province = manager?.offlineMapProvinceList?.find { it.provinceCode == provinceCode } val downloadedCityKeys = getDownloadedCityKeys(manager) province?.cityList?.map { city -> convertCityToMap(city, downloadedCityKeys) } ?: emptyList() } AsyncFunction("getDownloadedMaps") { val manager = getOfflineMapManager() val cities = manager?.downloadOfflineMapCityList ?: emptyList() val downloadedCityKeys = getDownloadedCityKeys(manager) cities.map { city -> convertCityToMap(city, downloadedCityKeys) } } // ==================== 下载管理 ==================== AsyncFunction("startDownload") { config: Map -> val cityCode = config["cityCode"] as? String ?: throw IllegalArgumentException("cityCode is required") synchronized(lock) { downloadingCities.add(cityCode) pausedCities.remove(cityCode) } getOfflineMapManager()?.downloadByCityCode(cityCode) } AsyncFunction("pauseDownload") { cityCode: String -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) synchronized(lock) { pausedCities.add(cityCode) downloadingCities.remove(cityCode) } // 使用 pauseByName 暂停指定城市 city?.city?.let { cityName -> getOfflineMapManager()?.pauseByName(cityName) } if (city != null) { sendEvent("onDownloadPaused", Bundle().apply { putString("cityCode", cityCode) putString("cityName", city.city) }) } } AsyncFunction("resumeDownload") { cityCode: String -> synchronized(lock) { downloadingCities.add(cityCode) pausedCities.remove(cityCode) } // Android SDK 没有针对单个城市的恢复方法 // 需要重新调用 downloadByCityCode 来继续下载 getOfflineMapManager()?.downloadByCityCode(cityCode) } AsyncFunction("cancelDownload") { cityCode: String -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) synchronized(lock) { downloadingCities.remove(cityCode) pausedCities.remove(cityCode) } // 使用 stop() 停止所有下载(包括队列) getOfflineMapManager()?.stop() if (city != null) { sendEvent("onDownloadCancelled", Bundle().apply { putString("cityCode", cityCode) putString("cityName", city.city) }) } } AsyncFunction("deleteMap") { cityCode: String -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) ?: throw IllegalArgumentException("City not found: $cityCode") // 官方文档:remove() 需要传入城市名称,不是城市代码 getOfflineMapManager()?.remove(city.city) synchronized(lock) { downloadingCities.remove(cityCode) pausedCities.remove(cityCode) } } AsyncFunction("updateMap") { cityCode: String -> synchronized(lock) { downloadingCities.add(cityCode) } getOfflineMapManager()?.updateOfflineCityByCode(cityCode) } AsyncFunction("checkUpdate") { cityCode: String -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) city?.state == OfflineMapStatus.NEW_VERSION } // ==================== 状态查询 ==================== AsyncFunction("isMapDownloaded") { cityCode: String -> val manager = getOfflineMapManager() val city = manager?.getItemByCityCode(cityCode) city?.state == OfflineMapStatus.SUCCESS || city?.let { isDownloadedCity(it, getDownloadedCityKeys(manager)) } == true } AsyncFunction("getMapStatus") { cityCode: String -> val manager = getOfflineMapManager() val city = manager?.getItemByCityCode(cityCode) val downloadedCityKeys = getDownloadedCityKeys(manager) city?.let { convertCityToMap(it, downloadedCityKeys) } ?: Bundle() } AsyncFunction("getTotalProgress") { val downloadedCities = getOfflineMapManager()?.downloadOfflineMapCityList ?: emptyList() if (downloadedCities.isEmpty()) { 0.0 } else { val totalProgress = downloadedCities.sumOf { getDownloadProgress(it).toDouble() } totalProgress / downloadedCities.size } } AsyncFunction("getDownloadingCities") { downloadingCities.toList() } // ==================== 存储管理 ==================== AsyncFunction("getStorageSize") { // 计算所有已下载地图的大小 val cities = getOfflineMapManager()?.downloadOfflineMapCityList ?: emptyList() cities.sumOf { it.size } } AsyncFunction("getStorageInfo") { val cities = getOfflineMapManager()?.downloadOfflineMapCityList ?: emptyList() val offlineMapSize = cities.sumOf { it.size } // 获取存储路径的统计信息 val stat = StatFs(Environment.getDataDirectory().path) val blockSize = stat.blockSizeLong val totalBlocks = stat.blockCountLong val availableBlocks = stat.availableBlocksLong val totalSpace = totalBlocks * blockSize val availableSpace = availableBlocks * blockSize val usedSpace = totalSpace - availableSpace Bundle().apply { putLong("totalSpace", totalSpace) putLong("usedSpace", usedSpace) putLong("availableSpace", availableSpace) putLong("offlineMapSize", offlineMapSize) } } AsyncFunction("clearAllMaps") { getOfflineMapManager()?.downloadOfflineMapCityList?.forEach { city -> // 使用城市名称删除 getOfflineMapManager()?.remove(city.city) } synchronized(lock) { downloadingCities.clear() pausedCities.clear() } } Function("setStoragePath") { _: String -> // Android 离线地图路径由系统管理,此方法不可用 } AsyncFunction("getStoragePath") { // Android 离线地图路径由系统管理 "" } // ==================== 批量操作 ==================== AsyncFunction("batchDownload") { cityCodes: List, _: Boolean? -> synchronized(lock) { cityCodes.forEach { cityCode -> downloadingCities.add(cityCode) pausedCities.remove(cityCode) } } cityCodes.forEach { cityCode -> getOfflineMapManager()?.downloadByCityCode(cityCode) } } AsyncFunction("batchDelete") { cityCodes: List -> cityCodes.forEach { cityCode -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) // 使用城市名称删除,不是城市代码 city?.city?.let { cityName -> getOfflineMapManager()?.remove(cityName) } } synchronized(lock) { cityCodes.forEach { cityCode -> downloadingCities.remove(cityCode) pausedCities.remove(cityCode) } } } AsyncFunction("batchUpdate") { cityCodes: List -> synchronized(lock) { cityCodes.forEach { cityCode -> downloadingCities.add(cityCode) } } cityCodes.forEach { cityCode -> getOfflineMapManager()?.updateOfflineCityByCode(cityCode) } } AsyncFunction("pauseAllDownloads") { // pause() 只暂停正在下载的,不包括队列 getOfflineMapManager()?.pause() synchronized(lock) { pausedCities.addAll(downloadingCities) downloadingCities.forEach { cityCode -> val city = getOfflineMapManager()?.getItemByCityCode(cityCode) if (city != null) { sendEvent("onDownloadPaused", Bundle().apply { putString("cityCode", cityCode) putString("cityName", city.city) }) } } downloadingCities.clear() } } AsyncFunction("resumeAllDownloads") { val pausedList = synchronized(lock) { pausedCities.toList() } // 重新下载所有暂停的城市 pausedList.forEach { cityCode -> synchronized(lock) { downloadingCities.add(cityCode) pausedCities.remove(cityCode) } getOfflineMapManager()?.downloadByCityCode(cityCode) } } } // ==================== 辅助方法 ==================== /** * 处理下载状态回调 */ private fun handleDownloadStatus(status: Int, completeCode: Int, downName: String?) { if (downName == null) return // downName 可能是城市代码或城市名称,尝试两种方式查找 var city = getOfflineMapManager()?.getItemByCityCode(downName) if (city == null) { city = getOfflineMapManager()?.offlineMapCityList?.find { it.city == downName } } if (city == null) return val cityCode = city.code val cityName = city.city when (status) { OfflineMapStatus.SUCCESS -> { synchronized(lock) { downloadingCities.remove(cityCode) pausedCities.remove(cityCode) } sendEvent("onDownloadComplete", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) }) } OfflineMapStatus.LOADING -> { sendEvent("onDownloadProgress", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) putInt("progress", completeCode) putLong("receivedSize", (city.size * completeCode / 100)) putLong("expectedSize", city.size) }) } OfflineMapStatus.UNZIP -> { sendEvent("onUnzipProgress", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) }) } OfflineMapStatus.ERROR -> { synchronized(lock) { downloadingCities.remove(cityCode) } sendEvent("onDownloadError", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) putString("error", "解压失败,数据可能有问题") }) } OfflineMapStatus.EXCEPTION_NETWORK_LOADING -> { sendEvent("onDownloadError", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) putString("error", "网络异常") }) } OfflineMapStatus.EXCEPTION_AMAP -> { synchronized(lock) { downloadingCities.remove(cityCode) } sendEvent("onDownloadError", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) putString("error", "认证异常,请检查Key") }) } OfflineMapStatus.EXCEPTION_SDCARD -> { synchronized(lock) { downloadingCities.remove(cityCode) } sendEvent("onDownloadError", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) putString("error", "SD卡读写异常") }) } OfflineMapStatus.PAUSE -> { // 检查是否是用户主动暂停 val isPaused = synchronized(lock) { pausedCities.contains(cityCode) } if (isPaused) { sendEvent("onDownloadPaused", Bundle().apply { putString("cityCode", cityCode) putString("cityName", cityName) }) } } } } /** * 转换城市对象为 Map */ private fun getDownloadedCityKeys(manager: OfflineMapManager?): Set { return manager?.downloadOfflineMapCityList ?.flatMap { getCityIdentityKeys(it) } ?.toSet() ?: emptySet() } private fun getCityIdentityKeys(city: OfflineMapCity): List { return listOfNotNull( city.code?.trim()?.takeIf { it.isNotEmpty() }?.let { "code:$it" }, city.adcode?.trim()?.takeIf { it.isNotEmpty() }?.let { "adcode:$it" }, city.city?.trim()?.takeIf { it.isNotEmpty() }?.let { "name:$it" } ) } private fun isDownloadedCity(city: OfflineMapCity, downloadedCityKeys: Set): Boolean { return getCityIdentityKeys(city).any { downloadedCityKeys.contains(it) } } private fun convertCityToMap( city: OfflineMapCity, downloadedCityKeys: Set = emptySet() ): Bundle { val isPaused = synchronized(lock) { pausedCities.contains(city.code) } val isDownloading = synchronized(lock) { downloadingCities.contains(city.code) } val isDownloaded = isDownloadedCity(city, downloadedCityKeys) val status = when { isPaused -> "paused" isDownloading -> "downloading" isDownloaded -> "downloaded" else -> getStatusString(city.state) } return Bundle().apply { putString("cityCode", city.code) putString("cityName", city.city) putLong("size", city.size) putString("status", status) putInt("progress", getDownloadProgress(city)) putString("version", city.version) putLong("downloadedSize", (city.size * getDownloadProgress(city) / 100)) } } /** * 转换省份对象为 Map */ private fun convertProvinceToMap(province: OfflineMapProvince): Bundle { return Bundle().apply { putString("cityCode", province.provinceCode) putString("cityName", province.provinceName) putLong("size", province.size) putString("status", getStatusString(province.state)) putInt("progress", getDownloadProgress(province)) putString("version", province.version) putString("provinceName", province.provinceName) putString("provinceCode", province.provinceCode) } } /** * 获取状态字符串。 * CHECKUPDATES / NEW_VERSION 在 10.1.600 冷启动时可能出现在普通城市列表, * 不能单独作为已下载依据;已下载状态以 downloadOfflineMapCityList 为准。 */ private fun getStatusString(state: Int): String { return when (state) { OfflineMapStatus.SUCCESS -> "downloaded" OfflineMapStatus.LOADING -> "downloading" OfflineMapStatus.UNZIP -> "unzipping" OfflineMapStatus.WAITING -> "downloading" OfflineMapStatus.PAUSE -> "paused" OfflineMapStatus.STOP -> "paused" OfflineMapStatus.ERROR -> "failed" OfflineMapStatus.EXCEPTION_NETWORK_LOADING -> "downloading" // 网络问题,可继续 OfflineMapStatus.EXCEPTION_AMAP -> "failed" // 认证异常 OfflineMapStatus.EXCEPTION_SDCARD -> "failed" // SD卡异常 startDownloadFailedCode -> "failed" // 兼容两种拼写的开始下载失败 OfflineMapStatus.CHECKUPDATES -> "not_downloaded" OfflineMapStatus.NEW_VERSION -> "not_downloaded" else -> "not_downloaded" } } /** * 按需初始化 OfflineMapManager: * - 隐私未同意时不初始化,避免应用启动阶段触发 555571 * - 用户同意后首次调用离线接口时自动初始化 */ private fun getOfflineMapManager(): OfflineMapManager? { offlineMapManager?.let { return it } val context = appContext.reactContext ?: return null return try { SDKInitializer.restorePersistedState(context) if (!SDKInitializer.isPrivacyReady()) { null } else { OfflineMapManager(context, object : OfflineMapManager.OfflineMapDownloadListener { override fun onDownload(status: Int, completeCode: Int, downName: String?) { handleDownloadStatus(status, completeCode, downName) } override fun onCheckUpdate(hasNew: Boolean, name: String?) { // 更新检查回调 } override fun onRemove(success: Boolean, name: String?, describe: String?) { // 删除回调 } }).also { manager -> offlineMapManager = manager } } } catch (e: Exception) { Log.w("ExpoGaodeMapOffline", "OfflineMapManager 初始化失败: ${e.message}") null } } private fun getActivityLaunchContext(): Context { return appContext.currentActivity ?: appContext.reactContext ?: throw CodedException("NO_CONTEXT", "React context not available", null) } private fun openOfflineMapUI() { val context = getActivityLaunchContext() SDKInitializer.restorePersistedState(context.applicationContext) if (!SDKInitializer.isPrivacyReady()) { throw CodedException( "PRIVACY_NOT_AGREED", "隐私协议未完成确认,请先调用 setPrivacyConfig", null ) } val intent = Intent(context, OfflineMapActivity::class.java) if (context !is Activity) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) } }