diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt index 80f7a591f0..486aaa0ef2 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt @@ -61,6 +61,7 @@ fun runOnMainLooper(forceQueue: Boolean = false, method: () -> Unit) { class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) : IGoogleMapDelegate.Stub() { internal val mapContext = MapContext(context) + private val compatAdapter = MapCompatAdapter(context) val view: FrameLayout var map: HuaweiMap? = null @@ -721,7 +722,8 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) Log.w(TAG, "onCreate: init TextureMapView error ", it) }.getOrDefault(MapView(mapContext, options.toHms())).apply { visibility = View.INVISIBLE } this.mapView = mapView - view.addView(mapView) + view.addView(compatAdapter.wrapMapView(mapContext, mapView)) + mapView.onCreate(savedInstanceState?.toHms()) mapView.getMapAsync(this::initMap) @@ -815,14 +817,8 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } map.setOnMapClickListener { latlng -> try { - if (options.liteMode) { - val parentView = view.parent?.parent - // TODO hms not support disable click listener when liteMode, this just fix for teams - if (parentView != null && parentView::class.qualifiedName.equals("com.microsoft.teams.location.ui.map.MapViewLite")) { - val clickView = parentView as ViewGroup - clickView.performClick() - return@setOnMapClickListener - } + if (options.liteMode && compatAdapter.interceptLiteModeClick(view)) { + return@setOnMapClickListener } mapClickListener?.onMapClick(latlng.toGms()) } catch (e: Exception) { @@ -831,14 +827,8 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } map.setOnMapLongClickListener { latlng -> try { - if (options.liteMode) { - val parentView = view.parent?.parent - // TODO hms not support disable click listener when liteMode, this just fix for teams - if (parentView != null && parentView::class.qualifiedName.equals("com.microsoft.teams.location.ui.map.MapViewLite")) { - val clickView = parentView as ViewGroup - clickView.performLongClick() - return@setOnMapLongClickListener - } + if (options.liteMode && compatAdapter.interceptLiteModeLongClick(view)) { + return@setOnMapLongClickListener } mapLongClickListener?.onMapLongClick(latlng.toGms()) } catch (e: Exception) { diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapCompatAdapter.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapCompatAdapter.kt new file mode 100644 index 0000000000..a24bde0606 --- /dev/null +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapCompatAdapter.kt @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.maps.hms.utils + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout + +/** + * Adapter that applies app-specific compatibility fixes for HMS Maps + * based on the calling application's package name. + */ +class MapCompatAdapter(context: Context) { + + private val callerPackageName: String = context.applicationContext?.packageName ?: context.packageName + + /** + * Wraps the mapView in a traversal-blocking container if needed. + * Some apps traverse the view hierarchy and encounter HMS internal views, + * causing crashes or unexpected behavior. + * + * @return the wrapper view to add to the root, or mapView itself if no wrapping is needed. + */ + fun wrapMapView(mapContext: Context, mapView: View): View { + if (!needsViewTraversalBlock().also { Log.d(TAG, "$callerPackageName need wrapMapView ? $it") }) return mapView + + val blocker = object : FrameLayout(mapContext) { + override fun getChildCount(): Int = 0 + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val lp = mapView.layoutParams + ?: LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + val childW = getChildMeasureSpec(widthMeasureSpec, 0, lp.width) + val childH = getChildMeasureSpec(heightMeasureSpec, 0, lp.height) + mapView.measure(childW, childH) + setMeasuredDimension( + resolveSize(mapView.measuredWidth, widthMeasureSpec), + resolveSize(mapView.measuredHeight, heightMeasureSpec) + ) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + mapView.layout(0, 0, right - left, bottom - top) + } + } + blocker.addView( + mapView, + ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) + ) + return blocker + } + + /** + * Intercepts map click events in liteMode for apps that need special handling. + * @return true if the click was intercepted and handled, false to proceed normally. + */ + fun interceptLiteModeClick(rootView: View): Boolean { + return findLiteModeParent(rootView)?.let { + it.performClick() + true + } ?: false + } + + /** + * Intercepts map long-click events in liteMode for apps that need special handling. + * @return true if the long-click was intercepted and handled, false to proceed normally. + */ + fun interceptLiteModeLongClick(rootView: View): Boolean { + return findLiteModeParent(rootView)?.let { + it.performLongClick() + true + } ?: false + } + + /** + * Determines if the mapView needs to be wrapped in a traversal-blocking container. + */ + private fun needsViewTraversalBlock(): Boolean { + return callerPackageName in VIEW_TRAVERSAL_BLOCK_PACKAGES + } + + /** + * Finds the parent view that should receive redirected click events in liteMode. + * Returns null if no redirection is needed for the current app. + */ + private fun findLiteModeParent(rootView: View): ViewGroup? { + val targetClass = LITE_MODE_CLICK_REDIRECT[callerPackageName] ?: return null + val parentView = rootView.parent?.parent ?: return null + if (parentView::class.qualifiedName == targetClass) { + return parentView as? ViewGroup + } + return null + } + + companion object { + private const val TAG = "MapCompatAdapter" + private val VIEW_TRAVERSAL_BLOCK_PACKAGES = setOf( + "com.studioeleven.windfinder", + ) + private val LITE_MODE_CLICK_REDIRECT = mapOf( + "com.microsoft.teams" to "com.microsoft.teams.location.ui.map.MapViewLite", + ) + } +} diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapContext.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapContext.kt index 00cb897918..2cddc40ebe 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapContext.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/MapContext.kt @@ -5,10 +5,13 @@ package org.microg.gms.maps.hms.utils +import android.annotation.SuppressLint import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.SharedPreferences +import android.content.res.Configuration +import android.content.res.Resources import android.view.LayoutInflater import androidx.annotation.RequiresApi import com.huawei.hms.maps.MapClientIdentify @@ -74,6 +77,75 @@ class MapContext(private val context: Context) : ContextWrapper(context.createPa return appContext.createDeviceProtectedStorageContext() } + private val appRes: Resources get() = appContext.resources + + internal val mergedResources: Resources by lazy(LazyThreadSafetyMode.NONE) { + val gmsRes = try { super.getResources() } catch (e: Exception) { return@lazy appRes } + @SuppressLint("DiscouragedApi") + @Suppress("DEPRECATION") + object : Resources(gmsRes.assets, gmsRes.displayMetrics, gmsRes.configuration) { + override fun getText(id: Int): CharSequence { + return try { + gmsRes.getText(id) + } catch (e: Exception) { + try { + appRes.getText(id) + } catch (e2: Exception) { + "" + } + } + } + + override fun getText(id: Int, def: CharSequence?): CharSequence { + return try { + gmsRes.getText(id, def) + } catch (e: Exception) { + try { + appRes.getText(id, def) + } catch (e2: Exception) { + def ?: "" + } + } + } + + override fun getString(id: Int): String { + return try { + gmsRes.getString(id) + } catch (e: Exception) { + try { + appRes.getString(id) + } catch (e2: Exception) { + "" + } + } + } + + override fun getString(id: Int, vararg formatArgs: Any?): String { + return try { + gmsRes.getString(id, *formatArgs) + } catch (e: Exception) { + try { + appRes.getString(id, *formatArgs) + } catch (e2: Exception) { + "" + } + } + } + } + } + + override fun getResources(): Resources = mergedResources + + override fun createConfigurationContext(overrideConfiguration: Configuration): Context { + val configCtx = super.createConfigurationContext(overrideConfiguration) + return object : ContextWrapper(configCtx) { + override fun getResources(): Resources = mergedResources + override fun createConfigurationContext(configuration: Configuration): Context { + return this@MapContext.createConfigurationContext(configuration) + } + } + } + companion object { val TAG = "GmsMapContext" }