diff --git a/play-services-base/core/src/main/java/org/microg/gms/utils/CoordinateConverter.java b/play-services-base/core/src/main/java/org/microg/gms/utils/CoordinateConverter.java new file mode 100644 index 0000000000..9aa4cd243c --- /dev/null +++ b/play-services-base/core/src/main/java/org/microg/gms/utils/CoordinateConverter.java @@ -0,0 +1,125 @@ +/** + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.utils; + +/** + * Conversion between the internationally accepted geographic coordinate system (WGS-84) + * and the geographic coordinate system used in China (GCJ-02) + */ +public class CoordinateConverter { + private static final double A = 6378245.0; + private static final double EE = 0.006693421622965943; + private static final double PI = Math.PI; + private static final double EPSILON = 1e-6; + private static final int MAX_ITERATIONS = 10; + private static final double PI_OVER_180 = PI / 180.0; + private static final double A_1_EE = A * (1 - EE); + + // ---------- WGS-84 to GCJ-02 ---------- + + /** + * Convert WGS-84 coordinates to GCJ-02 coordinates + * + * @param wgsLat latitude + * @param wgsLon longitude + * @return GCJ-02 coordinate array, index 0 is latitude, 1 is longitude + */ + public static double[] wgs84ToGcj02(double wgsLat, double wgsLon) { + if (isOutOfChina(wgsLat, wgsLon)) { + return new double[]{wgsLat, wgsLon}; + } + + double dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0); + double dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0); + double radLat = wgsLat * PI_OVER_180; + double magic = Math.sin(radLat); + magic = 1 - EE * magic * magic; + double sqrtMagic = Math.sqrt(magic); + + dLat = (dLat * 180.0) / (A_1_EE / (magic * sqrtMagic) * PI); + dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); + + return new double[]{wgsLat + dLat, wgsLon + dLon}; + } + + // ---------- GCJ-02 to WGS-84 ---------- + + /** + * Convert GCJ-02 coordinates to WGS-84 coordinates (iterative approximation method) + * + * @param gcjLat latitude + * @param gcjLon longitude + * @return WGS-84 coordinate array, index 0 is latitude, 1 is longitude + */ + public static double[] gcj02ToWgs84(double gcjLat, double gcjLon) { + if (isOutOfChina(gcjLat, gcjLon)) { + return new double[]{gcjLat, gcjLon}; + } + + double[] result = {gcjLat, gcjLon}; + double[] delta = new double[2]; + int iteration = 0; + + while (iteration++ < MAX_ITERATIONS) { + double[] gcjGuess = wgs84ToGcj02(result[0], result[1]); + delta[0] = gcjLat - gcjGuess[0]; + delta[1] = gcjLon - gcjGuess[1]; + + double h = 1e-4; + double[] gradLat = gradient(result[0], result[1], h, 0); + double[] gradLon = gradient(result[0], result[1], h, 1); + + double det = gradLat[0] * gradLon[1] - gradLat[1] * gradLon[0]; + if (Math.abs(det) < 1e-12) break; + + double stepLat = (delta[0] * gradLon[1] - delta[1] * gradLat[1]) / det; + double stepLon = (delta[1] * gradLat[0] - delta[0] * gradLon[0]) / det; + + result[0] += stepLat; + result[1] += stepLon; + + if (Math.abs(stepLat) < EPSILON && Math.abs(stepLon) < EPSILON) { + break; + } + } + return result; + } + + // numerical differentiation to compute gradients + private static double[] gradient(double lat, double lon, double h, int axis) { + double[] base = wgs84ToGcj02(lat, lon); + double[] delta; + if (axis == 0) { + delta = wgs84ToGcj02(lat + h, lon); + } else { + delta = wgs84ToGcj02(lat, lon + h); + } + return new double[]{(delta[0] - base[0]) / h, (delta[1] - base[1]) / h}; + } + + // determine whether the coordinates are outside China + private static boolean isOutOfChina(double lat, double lon) { + return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271; + } + + // latitude conversion formula + private static double transformLat(double x, double y) { + double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + // longitude conversion formula + private static double transformLon(double x, double y) { + double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); + ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } +} diff --git a/play-services-maps/core/hms/build.gradle b/play-services-maps/core/hms/build.gradle index 9ca2082e01..396b46de6e 100644 --- a/play-services-maps/core/hms/build.gradle +++ b/play-services-maps/core/hms/build.gradle @@ -9,6 +9,7 @@ apply plugin: 'kotlin-android' dependencies { implementation project(':play-services-base-core') implementation project(':play-services-maps') + implementation project(':play-services-location') implementation 'com.huawei.hms:maps:6.9.0.300' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 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 486aaa0ef2..2fb0aca73c 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 @@ -5,7 +5,9 @@ package org.microg.gms.maps.hms +import android.Manifest import android.content.Context +import android.content.pm.PackageManager import android.graphics.Bitmap import android.location.Location import android.os.* @@ -21,9 +23,12 @@ import android.widget.RelativeLayout import androidx.annotation.IdRes import androidx.annotation.Keep import androidx.collection.LongSparseArray +import androidx.core.app.ActivityCompat import com.google.android.gms.dynamic.IObjectWrapper import com.google.android.gms.dynamic.ObjectWrapper import com.google.android.gms.dynamic.unwrap +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationServices import com.google.android.gms.maps.GoogleMap.MAP_TYPE_TERRAIN import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.internal.* @@ -31,6 +36,7 @@ import com.google.android.gms.maps.model.* import com.google.android.gms.maps.model.internal.* import com.huawei.hms.maps.CameraUpdate import com.huawei.hms.maps.HuaweiMap +import com.huawei.hms.maps.LocationSource import com.huawei.hms.maps.MapView import com.huawei.hms.maps.MapsInitializer import com.huawei.hms.maps.OnMapReadyCallback @@ -42,8 +48,11 @@ import com.huawei.hms.maps.internal.IOnPoiClickListener import com.huawei.hms.maps.model.Marker import org.microg.gms.maps.hms.model.* import org.microg.gms.maps.hms.utils.* +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.Priority import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean +import com.google.android.gms.maps.model.LatLng private fun LongSparseArray.values() = (0 until size()).mapNotNull { valueAt(it) } @@ -102,6 +111,36 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) private var projectionImpl: ProjectionImpl? = null private var inDeveloperAnimation = false + private var locationEnabled: Boolean = false + private var isAddLocationCallback: Boolean = false + private var lastLocation: Location? = null + private var myLocationChangeListener: IOnMyLocationChangeListener? = null + + private val locationService by lazy { LocationServices.getFusedLocationProviderClient(context) } + private val locationCallback = LocationListener { location -> + lastLocation = location + try { + myLocationChangeListener?.onMyLocationChanged(ObjectWrapper.wrap(location)) + } catch (e: RemoteException) { + Log.w(TAG, "Failed to notify my-location listener", e) + } + val gcj02Location = Location(location).apply { + val hmsLatLng = LatLng(location.latitude, location.longitude).toHms() + latitude = hmsLatLng.latitude + longitude = hmsLatLng.longitude + } + mLocationChangedListener?.onLocationChanged(gcj02Location) + } + private var mLocationChangedListener: LocationSource.OnLocationChangedListener? = null + private var hwLocationSource: LocationSource = object : LocationSource { + override fun activate(listener: LocationSource.OnLocationChangedListener) { + mLocationChangedListener = listener + } + override fun deactivate() { + mLocationChangedListener = null + } + } + init { BitmapDescriptorFactoryImpl.initialize(context.resources) runOnMainLooper { @@ -340,7 +379,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun addMarker(options: MarkerOptions): IMarkerDelegate { val marker = MarkerImpl(this, "m${markerId++}", options) - if (map != null) { + if (map != null && initialized) { marker.update() } else { markers[marker.id] = marker @@ -425,16 +464,53 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setMyLocationEnabled(myLocation: Boolean) = afterInitialize { Log.d(TAG, "setMyLocationEnabled $myLocation") - it.isMyLocationEnabled = myLocation + synchronized(mapLock) { + locationEnabled = myLocation + try { + setLocationSource(null) + } catch (e: Exception) { + Log.w(TAG, e) + locationEnabled = false + } finally { + it.isMyLocationEnabled = locationEnabled + } + } } - override fun getMyLocation(): Location? { - Log.d(TAG, "deprecated Method: getMyLocation") - return null - } + override fun getMyLocation(): Location? = lastLocation override fun setLocationSource(locationSource: ILocationSourceDelegate?) = afterInitialize { - Log.d(TAG, "unimplemented Method: setLocationSource") + synchronized(mapLock) { + it.setLocationSource(hwLocationSource) + updateLocationEngineListener(locationEnabled) + } + } + + private fun updateLocationEngineListener(myLocation: Boolean) { + if (ActivityCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission( + context, Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + if (myLocation) { + if (!isAddLocationCallback) { + isAddLocationCallback = true + locationService.requestLocationUpdates( + LocationRequest.Builder(DEFAULT_LOCATION_INTERVAL_MILLIS) + .setPriority(Priority.PRIORITY_HIGH_ACCURACY) + .setMinUpdateIntervalMillis(DEFAULT_LOCATION_INTERVAL_MILLIS) + .setMaxUpdateDelayMillis(DEFAULT_LOCATION_INTERVAL_MILLIS) + .build(), locationCallback, Looper.getMainLooper() + ) + } + } else { + if (isAddLocationCallback) { + isAddLocationCallback = false + locationService.removeLocationUpdates(locationCallback) + } + } + } } override fun setContentDescription(desc: String?) = afterInitialize { @@ -507,7 +583,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setOnMarkerClickListener(listener: IOnMarkerClickListener?) = afterInitialize { hmap -> hmap.setOnMarkerClickListener { - Log.d("GmsGoogleMap", "setOnMarkerClickListener marker id -> ${it.id}") + Log.d(TAG, "setOnMarkerClickListener marker id -> ${it.id}") listener?.onMarkerClick(markers[it.id]) ?: false } } @@ -557,6 +633,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun setOnMyLocationChangeListener(listener: IOnMyLocationChangeListener?) = afterInitialize { Log.d(TAG, "deprecated Method: setOnMyLocationChangeListener") + myLocationChangeListener = listener } override fun setOnMyLocationButtonClickListener(listener: IOnMyLocationButtonClickListener?) = afterInitialize { @@ -630,11 +707,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) synchronized(mapLock) { if (loaded) { Log.d(TAG, "Invoking callback instantly, as map is loaded") - try { - scheduleExecute { callback.onMapLoaded() } - } catch (e: Exception) { - Log.w(TAG, e) - } + callback.scheduleExecute() } else { Log.d(TAG, "Delay callback invocation, as map is not yet loaded") loadedCallback = callback @@ -847,8 +920,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } internalOnInitializedCallbackList.clear() fakeWatermark { Log.d(TAG_LOGO, "fakeWatermark success") } - scheduleExecute { loadedCallback?.onMapLoaded() } - + loadedCallback?.scheduleExecute() mapView?.visibility = View.VISIBLE } @@ -862,6 +934,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) override fun onPause() = mapView?.onPause() ?: Unit override fun onDestroy() { Log.d(TAG, "onDestroy") + locationService.removeLocationUpdates(locationCallback) initializedCallbackList.clear() internalOnInitializedCallbackList.clear() circles.map { it.value.remove() } @@ -877,6 +950,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) // TODO can crash? mapView?.onDestroy() mapView = null + mLocationChangedListener = null // Don't make it null; this object is not deleted immediately, and it may want to access map.* stuff //map = null @@ -884,6 +958,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) created = false initialized = false loaded = false + isAddLocationCallback = false } override fun onStart() { @@ -929,6 +1004,16 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } } + private fun IOnMapLoadedCallback.scheduleExecute() { + Handler(Looper.getMainLooper()).postDelayed({ + try { + this.onMapLoaded() + } catch (e: Exception) { + Log.w(TAG, e) + } + }, ON_MAP_LOADED_CALLBACK_DELAY) + } + private var isInvokingInitializedCallbacks = AtomicBoolean(false) private fun tryRunUserInitializedCallbacks(tag: String = "") { @@ -955,7 +1040,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) Log.d("$TAG:$tag", "Invoking callback now, as map is initialized") val wasCallbackActive = isInvokingInitializedCallbacks.getAndSet(true) runOnMainLooper(forceQueue = wasCallbackActive) { - scheduleExecute { runCallbacks() } + runCallbacks() } if (!wasCallbackActive) isInvokingInitializedCallbacks.set(false) } else { @@ -975,17 +1060,12 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions) } - private fun scheduleExecute(block:() -> Unit) { - Handler(Looper.getMainLooper()).postDelayed({ - try { block.invoke() } catch (_: Exception) {} - }, ON_MAP_CALLBACK_DELAY) - } - companion object { private const val TAG = "GmsGoogleMap" private const val SNAPSHOT_OLD_VERSION_CODE = 4000000 private const val TAG_LOGO = "fakeWatermark" - private const val ON_MAP_CALLBACK_DELAY = 300L + private const val ON_MAP_LOADED_CALLBACK_DELAY = 500L + private const val DEFAULT_LOCATION_INTERVAL_MILLIS = 1000L } } diff --git a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt index dce21f082e..ab0d3793c2 100644 --- a/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt +++ b/play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/utils/typeConverter.kt @@ -14,6 +14,7 @@ import com.huawei.hms.maps.HuaweiMap import com.huawei.hms.maps.HuaweiMapOptions import com.huawei.hms.maps.model.* import org.microg.gms.maps.hms.R +import org.microg.gms.utils.CoordinateConverter import com.google.android.gms.maps.model.CameraPosition as GmsCameraPosition import com.google.android.gms.maps.model.CircleOptions as GmsCircleOptions import com.google.android.gms.maps.model.Dash as GmsDash @@ -79,7 +80,8 @@ fun GoogleMapOptions.toHms(): HuaweiMapOptions { } fun GmsLatLng.toHms(): LatLng = - LatLng(latitude, longitude) + runCatching { CoordinateConverter.wgs84ToGcj02(latitude, longitude) } + .getOrNull()?.let { LatLng(it[0], it[1]) } ?: LatLng(latitude, longitude) fun GmsLatLngBounds.toHms(): LatLngBounds = LatLngBounds( @@ -196,7 +198,9 @@ fun PatternItem.toGms(): GmsPatternItem = when (this) { else -> GmsGap(0f) } -fun LatLng.toGms(): GmsLatLng = GmsLatLng(latitude, longitude) +fun LatLng.toGms(): GmsLatLng = + runCatching { CoordinateConverter.gcj02ToWgs84(latitude, longitude) } + .getOrNull()?.let { GmsLatLng(it[0], it[1]) } ?: GmsLatLng(latitude, longitude) fun LatLngBounds.toGms(): GmsLatLngBounds = GmsLatLngBounds( GmsLatLng(southwest.latitude, southwest.longitude),