From 60a02903786dad1321596da91d5942c4889c0939 Mon Sep 17 00:00:00 2001 From: gunyu1019 Date: Fri, 3 Jul 2026 07:32:37 +0900 Subject: [PATCH 1/4] [Fix] Recover Android GLSurfaceView after resume --- .../flutter_kakao_maps/views/KakaoMapView.kt | 314 +++++++++++++++++- .../views/KakaoMapViewFactory.kt | 11 +- lib/widget/map_widget.dart | 34 +- test/kakao_map_widget_test.dart | 53 ++- 4 files changed, 396 insertions(+), 16 deletions(-) diff --git a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt index aa49cdf9..d891e1bc 100644 --- a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt +++ b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt @@ -3,37 +3,96 @@ package kr.yhs.flutter_kakao_maps.views import android.app.Activity import android.app.Application import android.content.Context +import android.content.ContextWrapper +import android.content.MutableContextWrapper +import android.hardware.display.DisplayManager import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.util.Log +import android.view.SurfaceHolder import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout import com.kakao.vectormap.KakaoMap import com.kakao.vectormap.MapView -import io.flutter.plugin.common.MethodChannel +import com.kakao.vectormap.camera.CameraPosition +import com.kakao.vectormap.camera.CameraUpdateFactory import io.flutter.plugin.platform.PlatformView import kr.yhs.flutter_kakao_maps.controller.KakaoMapController import kr.yhs.flutter_kakao_maps.model.KakaoMapOption +private const val PV_TAG = "PV-VD-DEBUG" + class KakaoMapView( private val activity: Activity, - private val context: Context, + context: Context, private val controller: KakaoMapController, - private val viewId: Int, private val option: KakaoMapOption, - private val channel: MethodChannel, + private val recreateMapViewOnResume: Boolean, + recreateMapViewDelayMillis: Long, + private val recoverGLSurfaceViewOnResume: Boolean, ) : PlatformView, Application.ActivityLifecycleCallbacks { - private val mapView = MapView(activity) + private val container = FrameLayout(context) + private val recreateDelayMillis = recreateMapViewDelayMillis.coerceAtLeast(0L) + private var mapView = createMapView(option) private lateinit var kakaoMap: KakaoMap + private var lastCameraPosition: CameraPosition? = null + private var pendingCameraPosition: CameraPosition? = null + private var isActivityResumed = false + private var wasActivityPaused = false + private var isDisposed = false + private var pendingRecreateRunnable: Runnable? = null + private var pendingRecovery = false + private var pendingRecoveryRunnable: Runnable? = null + private var pendingRecoveryTimeoutRunnable: Runnable? = null + + private val displayManager: DisplayManager = + context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + + private val displayListener = object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) { + val name = displayManager.getDisplay(displayId)?.name ?: "" + Log.i(PV_TAG, "[displayAdded] t=${SystemClock.uptimeMillis()} displayId=$displayId name=\"$name\"") + } + override fun onDisplayRemoved(displayId: Int) { + Log.i(PV_TAG, "[displayRemoved] t=${SystemClock.uptimeMillis()} displayId=$displayId") + } + override fun onDisplayChanged(displayId: Int) { + val name = displayManager.getDisplay(displayId)?.name ?: "" + Log.i(PV_TAG, "[displayChanged] t=${SystemClock.uptimeMillis()} displayId=$displayId name=\"$name\"") + } + } init { - controller.mapView = mapView - mapView.start(controller, option) + // Log construction: context class chain + val ctxChain = buildContextChain(context, 4) + val isMutable = context is MutableContextWrapper + Log.i(PV_TAG, "[init] t=${SystemClock.uptimeMillis()} view=KakaoMapView@${Integer.toHexString(System.identityHashCode(this))} container@${Integer.toHexString(System.identityHashCode(container))} contextChain=$ctxChain isMutableContext=$isMutable recreateOnResume=$recreateMapViewOnResume recoverGLSurfaceView=$recoverGLSurfaceViewOnResume") + if (recreateMapViewOnResume && recoverGLSurfaceViewOnResume) { + Log.i(PV_TAG, "[init] recreation overrides recovery") + } + + container.addView(mapView, matchParentLayoutParams()) activity.application.registerActivityLifecycleCallbacks(this) + displayManager.registerDisplayListener(displayListener, Handler(Looper.getMainLooper())) + + // Attach state listener on the container + container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + Log.i(PV_TAG, "[attach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") + } + override fun onViewDetachedFromWindow(v: View) { + Log.i(PV_TAG, "[detach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") + } + }) } - override fun getView(): View = mapView + override fun getView(): View = container override fun dispose() { - activity.application.unregisterActivityLifecycleCallbacks(this) - mapView.finish() + disposeMapView() controller.dispose() } @@ -44,11 +103,44 @@ class KakaoMapView( override fun onActivityResumed(activity: Activity) { if (activity != this.activity) return + val pendingRecreate = recreateMapViewOnResume && wasActivityPaused + val recoveryActive = recoverGLSurfaceViewOnResume && !recreateMapViewOnResume && wasActivityPaused + Log.i(PV_TAG, "[activityResumed] t=${SystemClock.uptimeMillis()} pendingRecreate=$pendingRecreate recoveryActive=$recoveryActive") + isActivityResumed = true + if (pendingRecreate) { + wasActivityPaused = false + scheduleMapViewRecreation() + return + } + if (recoveryActive) { + wasActivityPaused = false + // Resume normally first; recovery only adds the fork GL-thread un-pause if the + // engine rehost (detach/reattach) pauses the view again after this point. + mapView.resume() + pendingRecovery = true + val timeoutRunnable = Runnable { + pendingRecoveryTimeoutRunnable = null + if (pendingRecovery) { + pendingRecovery = false + Log.i(PV_TAG, "[recovery] no engine rehost detected within 600ms; skipping") + } + } + pendingRecoveryTimeoutRunnable = timeoutRunnable + container.postDelayed(timeoutRunnable, 600L) + return + } + wasActivityPaused = false mapView.resume() } override fun onActivityPaused(activity: Activity) { if (activity != this.activity) return + Log.i(PV_TAG, "[activityPaused] t=${SystemClock.uptimeMillis()} recreateOnResume=$recreateMapViewOnResume") + isActivityResumed = false + wasActivityPaused = true + cancelPendingMapViewRecreation() + cancelPendingRecovery() + captureCameraPosition() mapView.pause() } @@ -58,7 +150,209 @@ class KakaoMapView( override fun onActivityDestroyed(activity: Activity) { if (activity != this.activity) return + disposeMapView() + } + + private fun createMapView(startOption: KakaoMapOption): MapView { + val mapView = MapView(activity) + Log.i(PV_TAG, "[createMapView] t=${SystemClock.uptimeMillis()} mapView@${Integer.toHexString(System.identityHashCode(mapView))}") + val wrappedOption = startOption.also { it.setOnReady(::onMapReady) } + controller.mapView = mapView + + // Attach state listener on mapView + mapView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + Log.i(PV_TAG, "[attach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") + if (pendingRecovery) { + cancelPendingRecoveryTimeout() + val recoveryRunnable = Runnable { + pendingRecoveryRunnable = null + executeGLSurfaceViewRecovery() + } + pendingRecoveryRunnable = recoveryRunnable + container.post(recoveryRunnable) + } + } + override fun onViewDetachedFromWindow(v: View) { + Log.i(PV_TAG, "[detach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") + } + }) + + // Register SurfaceHolder.Callback on mapView's SurfaceView + runCatching { + val surfaceView = mapView.getSurfaceView() + surfaceView?.holder?.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + val surface = holder.surface + Log.i(PV_TAG, "[surfaceCreated] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid}") + } + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + val surface = holder.surface + Log.i(PV_TAG, "[surfaceChanged] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid} w=$width h=$height format=$format") + } + override fun surfaceDestroyed(holder: SurfaceHolder) { + val surface = holder.surface + Log.i(PV_TAG, "[surfaceDestroyed] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid}") + } + }) + }.onFailure { e -> + Log.i(PV_TAG, "[createMapView] surfaceHolder callback registration failed: $e") + } + + mapView.start(controller, wrappedOption) + return mapView + } + + private fun onMapReady(map: KakaoMap) { + kakaoMap = map + pendingCameraPosition?.let { cameraPosition -> + map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)) + lastCameraPosition = cameraPosition + pendingCameraPosition = null + } ?: run { + captureCameraPosition() + } + controller.onMapReady(map) + } + + private fun scheduleMapViewRecreation() { + Log.i(PV_TAG, "[scheduleRecreate] t=${SystemClock.uptimeMillis()} delayMs=$recreateDelayMillis") + cancelPendingMapViewRecreation() + val runnable = Runnable { + pendingRecreateRunnable = null + recreateMapView() + } + pendingRecreateRunnable = runnable + container.postDelayed(runnable, recreateDelayMillis) + } + + private fun cancelPendingMapViewRecreation() { + pendingRecreateRunnable?.let(container::removeCallbacks) + pendingRecreateRunnable = null + } + + private fun cancelPendingRecoveryTimeout() { + pendingRecoveryTimeoutRunnable?.let(container::removeCallbacks) + pendingRecoveryTimeoutRunnable = null + } + + private fun cancelPendingRecovery() { + cancelPendingRecoveryTimeout() + pendingRecoveryRunnable?.let(container::removeCallbacks) + pendingRecoveryRunnable = null + pendingRecovery = false + } + + private fun executeGLSurfaceViewRecovery() { + if (isDisposed) return + val surfaceView = runCatching { mapView.getSurfaceView() }.getOrNull() + val engineStateBefore = runCatching { mapView.getEngineState() }.getOrElse { "unavailable" } + Log.i(PV_TAG, "[recovery] engineState before=$engineStateBefore") + + // Primary: cast to Kakao fork GLSurfaceView and call onResume() + val castResult = runCatching { + val glSurfaceView = surfaceView as? com.kakao.vectormap.graphics.gl.GLSurfaceView + if (glSurfaceView != null) { + glSurfaceView.onResume() + Log.i(PV_TAG, "[recovery] fork GLSurfaceView.onResume path=cast") + true + } else { + false + } + }.getOrElse { e -> + Log.w(PV_TAG, "[recovery] cast path failed: $e") + false + } + + // Fallback: reflection if cast yielded null (e.g. Vulkan surface view) + if (!castResult && surfaceView != null) { + runCatching { + surfaceView.javaClass.getMethod("onResume").invoke(surfaceView) + Log.i(PV_TAG, "[recovery] fork GLSurfaceView.onResume path=reflect") + }.onFailure { e -> + Log.w(PV_TAG, "[recovery] reflect path failed: $e; continuing") + } + } + + runCatching { + mapView.resume() + Log.i(PV_TAG, "[recovery] calling MapView.resume") + }.onFailure { e -> + Log.w(PV_TAG, "[recovery] MapView.resume failed: $e") + } + + val engineStateAfter = runCatching { mapView.getEngineState() }.getOrElse { "unavailable" } + Log.i(PV_TAG, "[recovery] engineState after=$engineStateAfter") + pendingRecovery = false + } + + private fun recreateMapView() { + Log.i(PV_TAG, "[recreateMapView] t=${SystemClock.uptimeMillis()} isDisposed=$isDisposed") + if (isDisposed) return + val cameraPosition = captureCameraPosition() + pendingCameraPosition = cameraPosition + val oldMapView = mapView + oldMapView.finish() + container.removeView(oldMapView) + + val startOption = cameraPosition?.let(option::copyWithCameraPosition) ?: option + mapView = createMapView(startOption) + container.addView(mapView, matchParentLayoutParams()) + if (isActivityResumed) { + mapView.resume() + } + Log.i(PV_TAG, "[recreateMapView] done t=${SystemClock.uptimeMillis()} newMapView@${Integer.toHexString(System.identityHashCode(mapView))}") + } + + private fun captureCameraPosition(): CameraPosition? { + if (!::kakaoMap.isInitialized) return lastCameraPosition + lastCameraPosition = + runCatching { kakaoMap.cameraPosition }.getOrElse { lastCameraPosition } + return lastCameraPosition + } + + private fun disposeMapView() { + if (isDisposed) return + isDisposed = true + cancelPendingMapViewRecreation() + cancelPendingRecovery() mapView.finish() + container.removeAllViews() activity.application.unregisterActivityLifecycleCallbacks(this) + displayManager.unregisterDisplayListener(displayListener) + Log.i(PV_TAG, "[dispose] t=${SystemClock.uptimeMillis()} view=KakaoMapView@${Integer.toHexString(System.identityHashCode(this))}") + } + + private fun matchParentLayoutParams(): FrameLayout.LayoutParams { + return FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + + private fun buildContextChain(context: Context, maxDepth: Int): String { + val sb = StringBuilder() + var ctx: Context? = context + var depth = 0 + while (ctx != null && depth < maxDepth) { + if (depth > 0) sb.append('>') + sb.append(ctx.javaClass.simpleName) + ctx = if (ctx is ContextWrapper) ctx.baseContext else null + depth++ + } + return sb.toString() + } + + private fun buildParentChain(view: View, maxDepth: Int): String { + val sb = StringBuilder() + var v: ViewGroup? = view.parent as? ViewGroup + var depth = 0 + while (v != null && depth < maxDepth) { + if (depth > 0) sb.append('>') + sb.append(v.javaClass.simpleName) + v = v.parent as? ViewGroup + depth++ + } + return if (sb.isEmpty()) "" else sb.toString() } } diff --git a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapViewFactory.kt b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapViewFactory.kt index 0b000b9f..c38395bf 100644 --- a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapViewFactory.kt +++ b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapViewFactory.kt @@ -24,13 +24,20 @@ class KakaoMapViewFactory(private val activity: Activity, private val messenger: val controller = KakaoMapController(viewId, context, channel, overlayChannel) val option = KakaoMapOption.fromMessageable(controller::onMapReady, convertedArgs) + val recreateMapViewOnResume = + convertedArgs["recreateAndroidMapViewOnResume"] as Boolean? ?: false + val recreateMapViewDelayMillis = + (convertedArgs["androidMapViewRecreationDelayMillis"] as Number?)?.toLong() ?: 300L + val recoverGLSurfaceViewOnResume = + convertedArgs["recoverAndroidGLSurfaceViewOnResume"] as Boolean? ?: true return KakaoMapView( activity = activity, context = context, controller = controller, - viewId = viewId, option = option, - channel = channel, + recreateMapViewOnResume = recreateMapViewOnResume, + recreateMapViewDelayMillis = recreateMapViewDelayMillis, + recoverGLSurfaceViewOnResume = recoverGLSurfaceViewOnResume, ) } } diff --git a/lib/widget/map_widget.dart b/lib/widget/map_widget.dart index 272352e8..a4ddd27a 100644 --- a/lib/widget/map_widget.dart +++ b/lib/widget/map_widget.dart @@ -63,6 +63,20 @@ class KakaoMap extends StatefulWidget { /// 자세한 내용은 [Platform View](https://docs.flutter.dev/platform-integration/android/platform-views#texture-layer-or-texture-layer-hybrid-composition)을 참고해주세요. final bool forceHybridComposition; + /// Android에서 Activity resume 후 Flutter 엔진의 VirtualDisplay rehost 시 Kakao GLSurfaceView의 + /// GL 스레드를 비파괴적으로 재개하는 복구 옵션입니다. + /// [recreateAndroidMapViewOnResume]가 true인 경우 이 옵션은 비활성화되며, 파괴적 재생성이 우선합니다. + final bool recoverAndroidGLSurfaceViewOnResume; + + /// Android에서 앱이 백그라운드에서 복귀할 때 native MapView를 재생성합니다. + /// 이 옵션은 파괴적 비상 복구 수단입니다. [recoverAndroidGLSurfaceViewOnResume]가 비파괴적 기본 복구 경로입니다. + /// 이 옵션을 사용하면 복귀 시 [onMapReady]가 다시 호출되며, native 지도에 등록된 오버레이는 다시 구성해야 합니다. + final bool recreateAndroidMapViewOnResume; + + /// [recreateAndroidMapViewOnResume]가 활성화된 경우, Activity resume 이후 MapView 재생성을 지연할 시간입니다. + /// Flutter 엔진이 PlatformView의 Surface를 다시 연결한 뒤 재생성하기 위해 짧은 지연을 둡니다. + final Duration androidMapViewRecreationDelay; + const KakaoMap({ super.key, required this.onMapReady, @@ -79,6 +93,9 @@ class KakaoMap extends StatefulWidget { this.onTerrainLongClick, this.onMapError, this.forceHybridComposition = false, + this.recoverAndroidGLSurfaceViewOnResume = true, + this.recreateAndroidMapViewOnResume = false, + this.androidMapViewRecreationDelay = const Duration(milliseconds: 300), }); @override @@ -90,11 +107,21 @@ class _KakaoMapState extends State with KakaoMapControllerHandler { static const VIEW_TYPE = "plugin/kakao_map"; late final MethodChannel channel; late final KakaoMapController controller; + bool _hasNativeMapReady = false; @override Widget build(BuildContext context) { - Map rawParams = widget.option?.toMessageable() ?? + final Map rawParams = widget.option?.toMessageable() ?? (const KakaoMapOption()).toMessageable(); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + rawParams['recoverAndroidGLSurfaceViewOnResume'] = + widget.recoverAndroidGLSurfaceViewOnResume; + if (widget.recreateAndroidMapViewOnResume) { + rawParams['recreateAndroidMapViewOnResume'] = true; + rawParams['androidMapViewRecreationDelayMillis'] = + widget.androidMapViewRecreationDelay.inMilliseconds; + } + } // GestureRecognizer final Set> gestureRecognizers = {}; @@ -162,6 +189,11 @@ class _KakaoMapState extends State with KakaoMapControllerHandler { @override void onMapReady() { + final mapController = controller; + if (_hasNativeMapReady && mapController is KakaoMapControllerImplement) { + mapController._resetAfterNativeMapRecreation(); + } + _hasNativeMapReady = true; _setEventHandler(); controller.fetchBuildingHeightScale(); widget.onMapReady.call(controller); diff --git a/test/kakao_map_widget_test.dart b/test/kakao_map_widget_test.dart index 8041b2a4..7b53b297 100644 --- a/test/kakao_map_widget_test.dart +++ b/test/kakao_map_widget_test.dart @@ -111,11 +111,17 @@ void main() { expect(encodedAndroidParams, isNotNull); - final decodedAndroidParams = const StandardMessageCodec().decodeMessage( - ByteData.sublistView(encodedAndroidParams!), + final decodedAndroidParams = Map.from( + const StandardMessageCodec().decodeMessage( + ByteData.sublistView(encodedAndroidParams!), + ) as Map, ); - expect(decodedAndroidParams, equals(expectedParams)); + // Android always injects recoverAndroidGLSurfaceViewOnResume; strip it before comparing + // to the platform-agnostic expectedParams derived from KakaoMapOption.toMessageable(). + final androidParamsWithoutRecovery = Map.from(decodedAndroidParams) + ..remove('recoverAndroidGLSurfaceViewOnResume'); + expect(androidParamsWithoutRecovery, equals(expectedParams)); debugDefaultTargetPlatformOverride = TargetPlatform.iOS; @@ -133,4 +139,45 @@ void main() { debugDefaultTargetPlatformOverride = previous; } }); + + testWidgets('should pass Android MapView recreation params when enabled', ( + tester, + ) async { + final previous = debugDefaultTargetPlatformOverride; + + try { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + await tester.pumpWidget( + wrapApp( + KakaoMap( + key: UniqueKey(), + onMapReady: (_) {}, + recreateAndroidMapViewOnResume: true, + androidMapViewRecreationDelay: const Duration(milliseconds: 120), + ), + ), + ); + + expect(lastPlatformViewsCreateCall, isNotNull); + + final androidCreateArgs = Map.from( + lastPlatformViewsCreateCall!.arguments as Map, + ); + final encodedAndroidParams = androidCreateArgs['params'] as Uint8List?; + + expect(encodedAndroidParams, isNotNull); + + final decodedAndroidParams = Map.from( + const StandardMessageCodec().decodeMessage( + ByteData.sublistView(encodedAndroidParams!), + ) as Map, + ); + + expect(decodedAndroidParams['recreateAndroidMapViewOnResume'], isTrue); + expect(decodedAndroidParams['androidMapViewRecreationDelayMillis'], 120); + } finally { + debugDefaultTargetPlatformOverride = previous; + } + }); } From 740024ecbb11e5e83bb743265f0856506099ef26 Mon Sep 17 00:00:00 2001 From: gunyu1019 Date: Fri, 3 Jul 2026 07:32:38 +0900 Subject: [PATCH 2/4] [Feat] Add Android MapView recreation fallback --- .../model/KakaoMapOption.kt | 4 +++ lib/controller/controller_implement.dart | 32 +++++++++++++++++++ lib/controller/overlay/overlay_manager.dart | 4 +-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/model/KakaoMapOption.kt b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/model/KakaoMapOption.kt index 02bbb315..1c6cb6eb 100644 --- a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/model/KakaoMapOption.kt +++ b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/model/KakaoMapOption.kt @@ -5,6 +5,7 @@ import com.kakao.vectormap.KakaoMapReadyCallback import com.kakao.vectormap.LatLng import com.kakao.vectormap.MapType import com.kakao.vectormap.MapViewInfo +import com.kakao.vectormap.camera.CameraPosition import kr.yhs.flutter_kakao_maps.converter.CameraTypeConverter.asLatLng data class KakaoMapOption( @@ -40,6 +41,9 @@ data class KakaoMapOption( onReady = method } + fun copyWithCameraPosition(cameraPosition: CameraPosition): KakaoMapOption = + copy(initialPosition = cameraPosition.position, zoomLevel = cameraPosition.zoomLevel) + companion object { fun fromMessageable(onReady: ((KakaoMap) -> Unit), rawArgs: Map): KakaoMapOption { return KakaoMapOption( diff --git a/lib/controller/controller_implement.dart b/lib/controller/controller_implement.dart index 41168ec9..c19789e5 100644 --- a/lib/controller/controller_implement.dart +++ b/lib/controller/controller_implement.dart @@ -466,4 +466,36 @@ class KakaoMapControllerImplement extends KakaoMapController { Future resume() async { await channel.invokeMethod("resume"); } + + void _resetAfterNativeMapRecreation() { + for (final style in _poiStyle.values) { + style._isAdded = false; + } + for (final styles in _polygonStyle.values) { + for (final style in styles) { + style._isAdded = false; + } + } + for (final styles in _polylineStyle.values) { + for (final style in styles) { + style._isAdded = false; + } + } + for (final styles in _routeStyle.values) { + for (final style in styles) { + style._isAdded = false; + } + } + + _labelController.clear(); + _lodLabelController.clear(); + _shapeController.clear(); + _routeController.clear(); + _poiStyle.clear(); + _polygonStyle.clear(); + _polylineStyle.clear(); + _routeStyle.clear(); + buildingHeightScale = null; + _initalizeOverlayController(); + } } diff --git a/lib/controller/overlay/overlay_manager.dart b/lib/controller/overlay/overlay_manager.dart index 049663b0..f2157437 100644 --- a/lib/controller/overlay/overlay_manager.dart +++ b/lib/controller/overlay/overlay_manager.dart @@ -9,8 +9,8 @@ mixin OverlayManager { final Map _lodLabelController = {}; final Map _shapeController = {}; final Map _routeController = {}; - late final DimScreenController _dimController; - late final TrackingController _trackingController; + late DimScreenController _dimController; + late TrackingController _trackingController; // Overlay Style final Map _poiStyle = {}; From 8944615d63fded255a2f5750f08df83f63255529 Mon Sep 17 00:00:00 2001 From: gunyu1019 Date: Fri, 3 Jul 2026 08:28:53 +0900 Subject: [PATCH 3/4] [Fix] Remove unused logging and context management code in KakaoMapView --- .../flutter_kakao_maps/views/KakaoMapView.kt | 122 +----------------- 1 file changed, 2 insertions(+), 120 deletions(-) diff --git a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt index d891e1bc..48419ead 100644 --- a/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt +++ b/android/src/main/kotlin/kr/yhs/flutter_kakao_maps/views/KakaoMapView.kt @@ -3,15 +3,7 @@ package kr.yhs.flutter_kakao_maps.views import android.app.Activity import android.app.Application import android.content.Context -import android.content.ContextWrapper -import android.content.MutableContextWrapper -import android.hardware.display.DisplayManager import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.SystemClock -import android.util.Log -import android.view.SurfaceHolder import android.view.View import android.view.ViewGroup import android.widget.FrameLayout @@ -23,8 +15,6 @@ import io.flutter.plugin.platform.PlatformView import kr.yhs.flutter_kakao_maps.controller.KakaoMapController import kr.yhs.flutter_kakao_maps.model.KakaoMapOption -private const val PV_TAG = "PV-VD-DEBUG" - class KakaoMapView( private val activity: Activity, context: Context, @@ -48,45 +38,9 @@ class KakaoMapView( private var pendingRecoveryRunnable: Runnable? = null private var pendingRecoveryTimeoutRunnable: Runnable? = null - private val displayManager: DisplayManager = - context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager - - private val displayListener = object : DisplayManager.DisplayListener { - override fun onDisplayAdded(displayId: Int) { - val name = displayManager.getDisplay(displayId)?.name ?: "" - Log.i(PV_TAG, "[displayAdded] t=${SystemClock.uptimeMillis()} displayId=$displayId name=\"$name\"") - } - override fun onDisplayRemoved(displayId: Int) { - Log.i(PV_TAG, "[displayRemoved] t=${SystemClock.uptimeMillis()} displayId=$displayId") - } - override fun onDisplayChanged(displayId: Int) { - val name = displayManager.getDisplay(displayId)?.name ?: "" - Log.i(PV_TAG, "[displayChanged] t=${SystemClock.uptimeMillis()} displayId=$displayId name=\"$name\"") - } - } - init { - // Log construction: context class chain - val ctxChain = buildContextChain(context, 4) - val isMutable = context is MutableContextWrapper - Log.i(PV_TAG, "[init] t=${SystemClock.uptimeMillis()} view=KakaoMapView@${Integer.toHexString(System.identityHashCode(this))} container@${Integer.toHexString(System.identityHashCode(container))} contextChain=$ctxChain isMutableContext=$isMutable recreateOnResume=$recreateMapViewOnResume recoverGLSurfaceView=$recoverGLSurfaceViewOnResume") - if (recreateMapViewOnResume && recoverGLSurfaceViewOnResume) { - Log.i(PV_TAG, "[init] recreation overrides recovery") - } - container.addView(mapView, matchParentLayoutParams()) activity.application.registerActivityLifecycleCallbacks(this) - displayManager.registerDisplayListener(displayListener, Handler(Looper.getMainLooper())) - - // Attach state listener on the container - container.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) { - Log.i(PV_TAG, "[attach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") - } - override fun onViewDetachedFromWindow(v: View) { - Log.i(PV_TAG, "[detach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") - } - }) } override fun getView(): View = container @@ -105,7 +59,6 @@ class KakaoMapView( if (activity != this.activity) return val pendingRecreate = recreateMapViewOnResume && wasActivityPaused val recoveryActive = recoverGLSurfaceViewOnResume && !recreateMapViewOnResume && wasActivityPaused - Log.i(PV_TAG, "[activityResumed] t=${SystemClock.uptimeMillis()} pendingRecreate=$pendingRecreate recoveryActive=$recoveryActive") isActivityResumed = true if (pendingRecreate) { wasActivityPaused = false @@ -122,7 +75,6 @@ class KakaoMapView( pendingRecoveryTimeoutRunnable = null if (pendingRecovery) { pendingRecovery = false - Log.i(PV_TAG, "[recovery] no engine rehost detected within 600ms; skipping") } } pendingRecoveryTimeoutRunnable = timeoutRunnable @@ -135,7 +87,6 @@ class KakaoMapView( override fun onActivityPaused(activity: Activity) { if (activity != this.activity) return - Log.i(PV_TAG, "[activityPaused] t=${SystemClock.uptimeMillis()} recreateOnResume=$recreateMapViewOnResume") isActivityResumed = false wasActivityPaused = true cancelPendingMapViewRecreation() @@ -155,14 +106,11 @@ class KakaoMapView( private fun createMapView(startOption: KakaoMapOption): MapView { val mapView = MapView(activity) - Log.i(PV_TAG, "[createMapView] t=${SystemClock.uptimeMillis()} mapView@${Integer.toHexString(System.identityHashCode(mapView))}") val wrappedOption = startOption.also { it.setOnReady(::onMapReady) } controller.mapView = mapView - // Attach state listener on mapView mapView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { - Log.i(PV_TAG, "[attach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") if (pendingRecovery) { cancelPendingRecoveryTimeout() val recoveryRunnable = Runnable { @@ -173,32 +121,9 @@ class KakaoMapView( container.post(recoveryRunnable) } } - override fun onViewDetachedFromWindow(v: View) { - Log.i(PV_TAG, "[detach] t=${SystemClock.uptimeMillis()} view=${v.javaClass.simpleName}@${Integer.toHexString(System.identityHashCode(v))} displayId=${v.display?.displayId} parents=${buildParentChain(v, 5)}") - } + override fun onViewDetachedFromWindow(v: View) = Unit }) - // Register SurfaceHolder.Callback on mapView's SurfaceView - runCatching { - val surfaceView = mapView.getSurfaceView() - surfaceView?.holder?.addCallback(object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - val surface = holder.surface - Log.i(PV_TAG, "[surfaceCreated] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid}") - } - override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - val surface = holder.surface - Log.i(PV_TAG, "[surfaceChanged] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid} w=$width h=$height format=$format") - } - override fun surfaceDestroyed(holder: SurfaceHolder) { - val surface = holder.surface - Log.i(PV_TAG, "[surfaceDestroyed] t=${SystemClock.uptimeMillis()} surface@${Integer.toHexString(System.identityHashCode(surface))} isValid=${surface.isValid}") - } - }) - }.onFailure { e -> - Log.i(PV_TAG, "[createMapView] surfaceHolder callback registration failed: $e") - } - mapView.start(controller, wrappedOption) return mapView } @@ -216,7 +141,6 @@ class KakaoMapView( } private fun scheduleMapViewRecreation() { - Log.i(PV_TAG, "[scheduleRecreate] t=${SystemClock.uptimeMillis()} delayMs=$recreateDelayMillis") cancelPendingMapViewRecreation() val runnable = Runnable { pendingRecreateRunnable = null @@ -246,21 +170,17 @@ class KakaoMapView( private fun executeGLSurfaceViewRecovery() { if (isDisposed) return val surfaceView = runCatching { mapView.getSurfaceView() }.getOrNull() - val engineStateBefore = runCatching { mapView.getEngineState() }.getOrElse { "unavailable" } - Log.i(PV_TAG, "[recovery] engineState before=$engineStateBefore") // Primary: cast to Kakao fork GLSurfaceView and call onResume() val castResult = runCatching { val glSurfaceView = surfaceView as? com.kakao.vectormap.graphics.gl.GLSurfaceView if (glSurfaceView != null) { glSurfaceView.onResume() - Log.i(PV_TAG, "[recovery] fork GLSurfaceView.onResume path=cast") true } else { false } - }.getOrElse { e -> - Log.w(PV_TAG, "[recovery] cast path failed: $e") + }.getOrElse { false } @@ -268,26 +188,17 @@ class KakaoMapView( if (!castResult && surfaceView != null) { runCatching { surfaceView.javaClass.getMethod("onResume").invoke(surfaceView) - Log.i(PV_TAG, "[recovery] fork GLSurfaceView.onResume path=reflect") - }.onFailure { e -> - Log.w(PV_TAG, "[recovery] reflect path failed: $e; continuing") } } runCatching { mapView.resume() - Log.i(PV_TAG, "[recovery] calling MapView.resume") - }.onFailure { e -> - Log.w(PV_TAG, "[recovery] MapView.resume failed: $e") } - val engineStateAfter = runCatching { mapView.getEngineState() }.getOrElse { "unavailable" } - Log.i(PV_TAG, "[recovery] engineState after=$engineStateAfter") pendingRecovery = false } private fun recreateMapView() { - Log.i(PV_TAG, "[recreateMapView] t=${SystemClock.uptimeMillis()} isDisposed=$isDisposed") if (isDisposed) return val cameraPosition = captureCameraPosition() pendingCameraPosition = cameraPosition @@ -301,7 +212,6 @@ class KakaoMapView( if (isActivityResumed) { mapView.resume() } - Log.i(PV_TAG, "[recreateMapView] done t=${SystemClock.uptimeMillis()} newMapView@${Integer.toHexString(System.identityHashCode(mapView))}") } private fun captureCameraPosition(): CameraPosition? { @@ -319,8 +229,6 @@ class KakaoMapView( mapView.finish() container.removeAllViews() activity.application.unregisterActivityLifecycleCallbacks(this) - displayManager.unregisterDisplayListener(displayListener) - Log.i(PV_TAG, "[dispose] t=${SystemClock.uptimeMillis()} view=KakaoMapView@${Integer.toHexString(System.identityHashCode(this))}") } private fun matchParentLayoutParams(): FrameLayout.LayoutParams { @@ -329,30 +237,4 @@ class KakaoMapView( ViewGroup.LayoutParams.MATCH_PARENT, ) } - - private fun buildContextChain(context: Context, maxDepth: Int): String { - val sb = StringBuilder() - var ctx: Context? = context - var depth = 0 - while (ctx != null && depth < maxDepth) { - if (depth > 0) sb.append('>') - sb.append(ctx.javaClass.simpleName) - ctx = if (ctx is ContextWrapper) ctx.baseContext else null - depth++ - } - return sb.toString() - } - - private fun buildParentChain(view: View, maxDepth: Int): String { - val sb = StringBuilder() - var v: ViewGroup? = view.parent as? ViewGroup - var depth = 0 - while (v != null && depth < maxDepth) { - if (depth > 0) sb.append('>') - sb.append(v.javaClass.simpleName) - v = v.parent as? ViewGroup - depth++ - } - return if (sb.isEmpty()) "" else sb.toString() - } } From 37b32c8cb2eb14bdda84f5ab9ec20e4be6535b25 Mon Sep 17 00:00:00 2001 From: gunyu1019 Date: Fri, 3 Jul 2026 08:28:58 +0900 Subject: [PATCH 4/4] [Fix] Update .gitignore to include local Android engine debugging artifacts --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 194598dc..a082e771 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,8 @@ build/ .env .build/ -.swiftpm/ \ No newline at end of file +.swiftpm/ + +# Local Android engine debugging artifacts +engine_patches/ +example/android/engine_patch_repo/