From 5e99f0e79d64c2ef59afb74892e7a9173290aedd Mon Sep 17 00:00:00 2001 From: quan118 Date: Wed, 25 Sep 2024 15:54:35 +0700 Subject: [PATCH 1/2] Support background update --- android/src/main/AndroidManifest.xml | 8 +- .../geolocation/AndroidLocationManager.java | 478 +++++++++++------- .../geolocation/BaseLocationManager.java | 58 +-- .../geolocation/EventEmitter.java | 6 + .../geolocation/GeolocationModule.java | 28 +- .../geolocation/LocationHandler.java | 14 + .../geolocation/LocationOptions.java | 47 ++ .../geolocation/LocationService.java | 98 ++++ .../PlayServicesLocationManager.java | 354 ++++++++----- ios/RNCGeolocation.mm | 17 +- 10 files changed, 736 insertions(+), 372 deletions(-) create mode 100644 android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java create mode 100644 android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java create mode 100644 android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java create mode 100644 android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 868bb8b..6a1e39b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,6 +1,12 @@ - + + + \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java index 55d2619..eea43c3 100644 --- a/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java +++ b/android/src/main/java/com/reactnativecommunity/geolocation/AndroidLocationManager.java @@ -1,6 +1,5 @@ package com.reactnativecommunity.geolocation; -import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageManager; @@ -9,25 +8,29 @@ import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; +import android.os.Build; import android.os.Handler; - +import android.os.IBinder; +import android.content.ServiceConnection; +import android.content.Intent; +import android.content.ComponentName; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; -import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.common.ReactConstants; import com.facebook.react.common.SystemClock; import com.facebook.react.modules.core.DeviceEventManagerModule; -import javax.annotation.Nullable; - @SuppressLint("MissingPermission") public class AndroidLocationManager extends BaseLocationManager { - private @Nullable - String mWatchedProvider; + private boolean mIsServiceRunning = false; + private @Nullable LocationHandler mLocationHandler; private final LocationListener mLocationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { @@ -53,226 +56,339 @@ public void onProviderDisabled(String provider) { } }; - protected AndroidLocationManager(ReactApplicationContext reactContext) { - super(reactContext); + private final ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + LocationService.LocalBinder binder = (LocationService.LocalBinder) service; + mLocationHandler = binder.getService(); + final AndroidLocationManager.LocationHandlerImpl locationHandlerImpl = + new AndroidLocationManager.LocationHandlerImpl( + (LocationService)mLocationHandler, + mLocationListener, + AndroidLocationManager.this); + ((LocationService) mLocationHandler).setLocationHandler(locationHandlerImpl); + mIsServiceRunning = true; + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + mLocationHandler = null; + mIsServiceRunning = false; + } + }; + + protected AndroidLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) { + super(reactContext, enableBackgroundLocationUpdates); + Log.d("AndroidLocationManager", "enableBackgroundLocationUpdates:" + enableBackgroundLocationUpdates); + if (enableBackgroundLocationUpdates) { + startService(); + } else { + mLocationHandler = new LocationHandlerImpl(reactContext, mLocationListener, this); + } } public void getCurrentLocationData( ReadableMap options, final Callback success, Callback error) { - AndroidLocationManager.LocationOptions locationOptions = AndroidLocationManager.LocationOptions.fromReactMap(options); + LocationOptions locationOptions = LocationOptions.fromReactMap(options); try { - LocationManager locationManager = - (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE); - String provider = getValidProvider(locationManager, locationOptions.highAccuracy); - if (provider == null) { - error.invoke( - PositionError.buildError( - PositionError.POSITION_UNAVAILABLE, "No location provider available.")); - return; - } - Location location = locationManager.getLastKnownLocation(provider); - if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) { - success.invoke(locationToMap(location)); - return; + if (mIsServiceRunning) { + if (!mEnableBackgroundLocationUpdates) { + stopService(); + mLocationHandler = new LocationHandlerImpl(mReactContext, mLocationListener, this); + } else if (mLocationHandler == null) { + mLocationHandler = new LocationHandlerImpl(mReactContext, mLocationListener, this); + } + } else { + if (mEnableBackgroundLocationUpdates) { + startService(); + } } - new AndroidLocationManager.SingleUpdateRequest(locationManager, provider, locationOptions.timeout, success, error) - .invoke(location); + mLocationHandler.getCurrentLocation(locationOptions, success, error); } catch (SecurityException e) { throw e; } } + @Override public void startObserving(ReadableMap options) { - if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) { + LocationOptions locationOptions = LocationOptions.fromReactMap(options); + mLocationHandler.startLocationUpdates(locationOptions); + } + + @Override + public void stopObserving() { +// LocationManager locationManager = +// (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE); +// locationManager.removeUpdates(mLocationListener); +// mWatchedProvider = null; + mLocationHandler.stopLocationUpdates(); + } + + private void startService() { + if (mIsServiceRunning) { return; } - LocationOptions locationOptions = LocationOptions.fromReactMap(options); - try { - LocationManager locationManager = - (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE); - String provider = getValidProvider(locationManager, locationOptions.highAccuracy); - if (provider == null) { - emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available."); - return; - } - if (!provider.equals(mWatchedProvider)) { - locationManager.removeUpdates(mLocationListener); - locationManager.requestLocationUpdates( - provider, - 1000, - locationOptions.distanceFilter, - mLocationListener); - } - mWatchedProvider = provider; - } catch (SecurityException e) { - throw e; + Intent intent = new Intent(mReactContext, LocationService.class); + + // Start the LocationService as a foreground service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mReactContext.startForegroundService(intent); + } else { + mReactContext.startService(intent); } + + mReactContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } - public void stopObserving() { - LocationManager locationManager = - (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE); - locationManager.removeUpdates(mLocationListener); - mWatchedProvider = null; + @Override + public void stopService() { + if (!mIsServiceRunning) { + return; + } + + mReactContext.unbindService(mConnection); + Intent intent = new Intent(mReactContext, LocationService.class); + mReactContext.stopService(intent); } - @Nullable - private String getValidProvider(LocationManager locationManager, boolean highAccuracy) { - String provider = - highAccuracy ? LocationManager.GPS_PROVIDER : LocationManager.NETWORK_PROVIDER; - if (!locationManager.isProviderEnabled(provider)) { - provider = provider.equals(LocationManager.GPS_PROVIDER) - ? LocationManager.NETWORK_PROVIDER - : LocationManager.GPS_PROVIDER; - if (!locationManager.isProviderEnabled(provider)) { - return null; + private static class LocationHandlerImpl implements LocationHandler { + private static final String TAG = "AndroidLocationHandlerImpl"; + private final LocationListener mLocationListener; + private final Context mContext; + private @Nullable String mWatchedProvider; + private final EventEmitter mEventEmitter; + + public LocationHandlerImpl(Context context, LocationListener locationListener, EventEmitter errorEmitter) { + mContext = context; + mLocationListener = locationListener; + mEventEmitter = errorEmitter; + } + + public void startLocationUpdates(LocationOptions options) { + Log.i(TAG, "startLocationUpdates"); + if (LocationManager.GPS_PROVIDER.equals(mWatchedProvider)) { + return; + } + + try { + LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + String provider = getValidProvider(locationManager, options.highAccuracy); + if (provider == null) { + // handle error + // emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available."); + mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "No location provider available."); + return; + } + if (!provider.equals(mWatchedProvider)) { + locationManager.removeUpdates(mLocationListener); + locationManager.requestLocationUpdates( + provider, + 1000, + options.distanceFilter, + mLocationListener); + } + mWatchedProvider = provider; + } catch (SecurityException e) { + throw e; } } - // If it's an enabled provider, but we don't have permissions, ignore it - int finePermission = ContextCompat.checkSelfPermission(mReactContext, android.Manifest.permission.ACCESS_FINE_LOCATION); - int coarsePermission = ContextCompat.checkSelfPermission(mReactContext, android.Manifest.permission.ACCESS_COARSE_LOCATION); - if ((provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) || - (provider.equals(LocationManager.NETWORK_PROVIDER) && coarsePermission != PackageManager.PERMISSION_GRANTED)) { - return null; + + public void stopLocationUpdates() { + Log.i(TAG, "stopLocationUpdates"); + LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + locationManager.removeUpdates(mLocationListener); + mWatchedProvider = null; } - return provider; - } - private static class SingleUpdateRequest { - - private final Callback mSuccess; - private final Callback mError; - private final LocationManager mLocationManager; - private final String mProvider; - private final long mTimeout; - private Location mOldLocation; - private final Handler mHandler = new Handler(); - private final Runnable mTimeoutRunnable = new Runnable() { - @Override - public void run() { - synchronized (SingleUpdateRequest.this) { - if (!mTriggered) { - mError.invoke(PositionError.buildError(PositionError.TIMEOUT, "Location request timed out")); - mLocationManager.removeUpdates(mLocationListener); - FLog.i(ReactConstants.TAG, "LocationModule: Location request timed out"); - mTriggered = true; - } + public void getCurrentLocation(LocationOptions options, + final Callback success, + Callback error) { + Log.i(TAG, "getCurrentLocation"); + + try { + LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + + String provider = getValidProvider(locationManager, options.highAccuracy); + if (provider == null) { + error.invoke( + PositionError.buildError( + PositionError.POSITION_UNAVAILABLE, "No location provider available.")); + return; } - } - }; - private final LocationListener mLocationListener = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - synchronized (SingleUpdateRequest.this) { - if (!mTriggered && isBetterLocation(location, mOldLocation)) { - mSuccess.invoke(locationToMap(location)); - mHandler.removeCallbacks(mTimeoutRunnable); - mTriggered = true; - mLocationManager.removeUpdates(mLocationListener); - } - mOldLocation = location; + Location location = locationManager.getLastKnownLocation(provider); + if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < options.maximumAge) { + success.invoke(locationToMap(location)); + return; } - } - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { + new LocationHandlerImpl.SingleUpdateRequest(locationManager, provider, options.timeout, success, error).invoke(location); + } catch (SecurityException e) { + throw e; } + } + + @Nullable + private String getValidProvider(LocationManager locationManager, boolean highAccuracy) { + String provider = highAccuracy ? LocationManager.GPS_PROVIDER : LocationManager.NETWORK_PROVIDER; + if (!locationManager.isProviderEnabled(provider)) { + provider = provider.equals(LocationManager.GPS_PROVIDER) + ? LocationManager.NETWORK_PROVIDER + : LocationManager.GPS_PROVIDER; - @Override - public void onProviderEnabled(String provider) { + if (!locationManager.isProviderEnabled(provider)) { + return null; + } } - @Override - public void onProviderDisabled(String provider) { + // If it's an enabled provider, but we don't have permissions, ignore it + int finePermission = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.ACCESS_FINE_LOCATION); + int coarsePermission = ContextCompat.checkSelfPermission(mContext, android.Manifest.permission.ACCESS_COARSE_LOCATION); + + if ((provider.equals(LocationManager.GPS_PROVIDER) && finePermission != PackageManager.PERMISSION_GRANTED) || + (provider.equals(LocationManager.NETWORK_PROVIDER) && coarsePermission != PackageManager.PERMISSION_GRANTED)) { + return null; } - }; - private boolean mTriggered; - - private SingleUpdateRequest( - LocationManager locationManager, - String provider, - long timeout, - Callback success, - Callback error) { - mLocationManager = locationManager; - mProvider = provider; - mTimeout = timeout; - mSuccess = success; - mError = error; - } - public void invoke(Location location) { - mOldLocation = location; - mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener); - mHandler.postDelayed(mTimeoutRunnable, mTimeout); + return provider; } - private static final int TWO_MINUTES = 1000 * 60 * 2; - - /** - * Determines whether one Location reading is better than the current Location fix - * taken from Android Examples https://developer.android.com/guide/topics/location/strategies.html - * - * @param location The new Location that you want to evaluate - * @param currentBestLocation The current Location fix, to which you want to compare the new one - */ - private boolean isBetterLocation(Location location, Location currentBestLocation) { - if (currentBestLocation == null) { - // A new location is always better than no location - return true; - } + private static class SingleUpdateRequest { + private final Callback mSuccess; + private final Callback mError; + private final LocationManager mLocationManager; + private final String mProvider; + private final long mTimeout; + private Location mOldLocation; + private final Handler mHandler = new Handler(); + private final Runnable mTimeoutRunnable = new Runnable() { + @Override + public void run() { + synchronized (SingleUpdateRequest.this) { + if (!mTriggered) { + mError.invoke(PositionError.TIMEOUT, "Location request timed out"); + mLocationManager.removeUpdates(mLocationListener); + mTriggered = true; + } + } + } + }; + private final LocationListener mLocationListener = new LocationListener() { + @Override + public void onLocationChanged(@NonNull Location location) { + synchronized (SingleUpdateRequest.this) { + if (!mTriggered && isBetterLocation(location, mOldLocation)) { + mSuccess.invoke(location); + mHandler.removeCallbacks(mTimeoutRunnable); + mTriggered = true; + mLocationManager.removeUpdates(mLocationListener); + } + + mOldLocation = location; + } + } - // Check whether the new location fix is newer or older - long timeDelta = location.getTime() - currentBestLocation.getTime(); - boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; - boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; - boolean isNewer = timeDelta > 0; - - // If it's been more than two minutes since the current location, use the new location - // because the user has likely moved - if (isSignificantlyNewer) { - return true; - // If the new location is more than two minutes older, it must be worse - } else if (isSignificantlyOlder) { - return false; + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } + }; + private boolean mTriggered; + + private SingleUpdateRequest( + LocationManager locationManager, + String provider, + long timeout, + Callback success, + Callback error) { + mLocationManager = locationManager; + mProvider = provider; + mTimeout = timeout; + mSuccess = success; + mError = error; } - // Check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - boolean isLessAccurate = accuracyDelta > 0; - boolean isMoreAccurate = accuracyDelta < 0; - boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // Check if the old and new location are from the same provider - boolean isFromSameProvider = isSameProvider(location.getProvider(), - currentBestLocation.getProvider()); - - // Determine location quality using a combination of timeliness and accuracy - if (isMoreAccurate) { - return true; - } else if (isNewer && !isLessAccurate) { - return true; - } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { - return true; + public void invoke(Location location) { + mOldLocation = location; + mLocationManager.requestLocationUpdates(mProvider, 100, 1, mLocationListener); + mHandler.postDelayed(mTimeoutRunnable, mTimeout); } - return false; - } + private static final int TWO_MINUTES = 1000 * 60 * 2; + + /** + * Determines whether one Location reading is better than the current Location fix + * taken from Android Examples https://developer.android.com/guide/topics/location/strategies.html + * + * @param location The new Location that you want to evaluate + * @param currentBestLocation The current Location fix, to which you want to compare the new one + */ + private boolean isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { + // A new location is always better than no location + return true; + } + + // Check whether the new location fix is newer or older + long timeDelta = location.getTime() - currentBestLocation.getTime(); + boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; + boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; + boolean isNewer = timeDelta > 0; + + // If it's been more than two minutes since the current location, use the new location + // because the user has likely moved + if (isSignificantlyNewer) { + return true; + // If the new location is more than two minutes older, it must be worse + } else if (isSignificantlyOlder) { + return false; + } + + // Check whether the new location fix is more or less accurate + int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); + boolean isLessAccurate = accuracyDelta > 0; + boolean isMoreAccurate = accuracyDelta < 0; + boolean isSignificantlyLessAccurate = accuracyDelta > 200; + + // Check if the old and new location are from the same provider + boolean isFromSameProvider = isSameProvider(location.getProvider(), + currentBestLocation.getProvider()); + + // Determine location quality using a combination of timeliness and accuracy + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { + return true; + } - /** - * Checks whether two providers are the same - */ - private boolean isSameProvider(String provider1, String provider2) { - if (provider1 == null) { - return provider2 == null; + return false; + } + + /** + * Checks whether two providers are the same + */ + private boolean isSameProvider(String provider1, String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); } - return provider1.equals(provider2); } } } diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java index 02d2e85..8947e99 100644 --- a/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java +++ b/android/src/main/java/com/reactnativecommunity/geolocation/BaseLocationManager.java @@ -11,12 +11,13 @@ import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; -public abstract class BaseLocationManager { - protected static final float RCT_DEFAULT_LOCATION_ACCURACY = 100; +public abstract class BaseLocationManager implements EventEmitter { public ReactApplicationContext mReactContext; + public boolean mEnableBackgroundLocationUpdates; - protected BaseLocationManager(ReactApplicationContext reactContext) { + protected BaseLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) { mReactContext = reactContext; + mEnableBackgroundLocationUpdates = enableBackgroundLocationUpdates; } protected static WritableMap locationToMap(Location location) { @@ -70,55 +71,18 @@ protected static void putIntoMap(WritableMap map, String key, Object value) { } } - protected void emitError(int code, String message) { + public void emitError(int code, String message) { mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("geolocationError", PositionError.buildError(code, message)); } + public void emit(String message, Object o) { + mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(message, o); + } + abstract public void getCurrentLocationData(ReadableMap options, final Callback success, Callback error); abstract public void startObserving(ReadableMap options); abstract public void stopObserving(); - - protected static class LocationOptions { - protected final int interval; - protected final int fastestInterval; - protected final long timeout; - protected final double maximumAge; - protected final boolean highAccuracy; - protected final float distanceFilter; - - private LocationOptions( - int interval, - int fastestInterval, - long timeout, - double maximumAge, - boolean highAccuracy, - float distanceFilter) { - this.interval = interval; - this.fastestInterval = fastestInterval; - this.timeout = timeout; - this.maximumAge = maximumAge; - this.highAccuracy = highAccuracy; - this.distanceFilter = distanceFilter; - } - - protected static LocationOptions fromReactMap(ReadableMap map) { - // precision might be dropped on timeout (double -> int conversion), but that's OK - int interval = - map.hasKey("interval") ? map.getInt("interval") : 10000; - int fastestInterval = - map.hasKey("fastestInterval") ? map.getInt("fastestInterval") : -1; - long timeout = - map.hasKey("timeout") ? (long) map.getDouble("timeout") : 1000 * 60 * 10; - double maximumAge = - map.hasKey("maximumAge") ? map.getDouble("maximumAge") : Double.POSITIVE_INFINITY; - boolean highAccuracy = - map.hasKey("enableHighAccuracy") && map.getBoolean("enableHighAccuracy"); - float distanceFilter = map.hasKey("distanceFilter") ? - (float) map.getDouble("distanceFilter") : - RCT_DEFAULT_LOCATION_ACCURACY; - - return new LocationOptions(interval, fastestInterval, timeout, maximumAge, highAccuracy, distanceFilter); - } - } + abstract public void stopService(); } diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java b/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java new file mode 100644 index 0000000..dfca465 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/geolocation/EventEmitter.java @@ -0,0 +1,6 @@ +package com.reactnativecommunity.geolocation; + +public interface EventEmitter { + void emitError(int code, String message); + void emit(String message, Object obj); +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java b/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java index 5b42ba4..3e87a5e 100644 --- a/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java +++ b/android/src/main/java/com/reactnativecommunity/geolocation/GeolocationModule.java @@ -35,7 +35,7 @@ public class GeolocationModule extends ReactContextBaseJavaModule { public GeolocationModule(ReactApplicationContext reactContext) { super(reactContext); mConfiguration = Configuration.getDefault(); - mLocationManager = new AndroidLocationManager(reactContext); + mLocationManager = new AndroidLocationManager(reactContext, mConfiguration.enableBackgroundLocationUpdates); } @Override @@ -44,20 +44,25 @@ public String getName() { } public void setConfiguration(ReadableMap config) { - mConfiguration = Configuration.fromReactMap(config); - onConfigurationChange(mConfiguration); + Configuration configuration = Configuration.fromReactMap(config); + onConfigurationChange(configuration); } private void onConfigurationChange(Configuration config) { ReactApplicationContext reactContext = mLocationManager.mReactContext; - if (Objects.equals(config.locationProvider, "android") && mLocationManager instanceof PlayServicesLocationManager) { - mLocationManager = new AndroidLocationManager(reactContext); - } else if (Objects.equals(config.locationProvider, "playServices") && mLocationManager instanceof AndroidLocationManager) { + if (Objects.equals(config.locationProvider, "android") && mLocationManager instanceof PlayServicesLocationManager + || config.enableBackgroundLocationUpdates != mConfiguration.enableBackgroundLocationUpdates) { + mLocationManager.stopService(); + mLocationManager = new AndroidLocationManager(reactContext, config.enableBackgroundLocationUpdates); + } else if (Objects.equals(config.locationProvider, "playServices") && mLocationManager instanceof AndroidLocationManager + || config.enableBackgroundLocationUpdates != mConfiguration.enableBackgroundLocationUpdates) { GoogleApiAvailability availability = new GoogleApiAvailability(); if (availability.isGooglePlayServicesAvailable(reactContext.getApplicationContext()) == ConnectionResult.SUCCESS) { - mLocationManager = new PlayServicesLocationManager(reactContext); + mLocationManager.stopService(); + mLocationManager = new PlayServicesLocationManager(reactContext, config.enableBackgroundLocationUpdates); } } + mConfiguration = config; } /** @@ -172,14 +177,16 @@ private void emitLocationPermissionMissing(SecurityException e) { private static class Configuration { String locationProvider; Boolean skipPermissionRequests; + Boolean enableBackgroundLocationUpdates; - private Configuration(String locationProvider, boolean skipPermissionRequests) { + private Configuration(String locationProvider, boolean skipPermissionRequests, boolean enableBackgroundLocationUpdates) { this.locationProvider = locationProvider; this.skipPermissionRequests = skipPermissionRequests; + this.enableBackgroundLocationUpdates = enableBackgroundLocationUpdates; } protected static Configuration getDefault() { - return new Configuration("auto", false); + return new Configuration("auto", false, false); } protected static Configuration fromReactMap(ReadableMap map) { @@ -187,7 +194,8 @@ protected static Configuration fromReactMap(ReadableMap map) { map.hasKey("locationProvider") ? map.getString("locationProvider") : "auto"; boolean skipPermissionRequests = map.hasKey("skipPermissionRequests") ? map.getBoolean("skipPermissionRequests") : false; - return new Configuration(locationProvider, skipPermissionRequests); + boolean enableBackgroundLocationUpdates = map.hasKey("enableBackgroundLocationUpdates") ? map.getBoolean("enableBackgroundLocationUpdates") : false; + return new Configuration(locationProvider, skipPermissionRequests, enableBackgroundLocationUpdates); } } } diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java new file mode 100644 index 0000000..f7f3931 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationHandler.java @@ -0,0 +1,14 @@ +package com.reactnativecommunity.geolocation; + +import com.facebook.react.bridge.Callback; + +public interface LocationHandler { + void getCurrentLocation(LocationOptions options, Callback success, Callback error); + + void startLocationUpdates(LocationOptions options); + + void stopLocationUpdates(); +} + + + diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java new file mode 100644 index 0000000..3e821b5 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationOptions.java @@ -0,0 +1,47 @@ +package com.reactnativecommunity.geolocation; + +import com.facebook.react.bridge.ReadableMap; + +public class LocationOptions { + protected static final float RCT_DEFAULT_LOCATION_ACCURACY = 100; + protected final int interval; + protected final int fastestInterval; + protected final long timeout; + protected final double maximumAge; + protected final boolean highAccuracy; + protected final float distanceFilter; + + private LocationOptions( + int interval, + int fastestInterval, + long timeout, + double maximumAge, + boolean highAccuracy, + float distanceFilter) { + this.interval = interval; + this.fastestInterval = fastestInterval; + this.timeout = timeout; + this.maximumAge = maximumAge; + this.highAccuracy = highAccuracy; + this.distanceFilter = distanceFilter; + } + + protected static LocationOptions fromReactMap(ReadableMap map) { + // precision might be dropped on timeout (double -> int conversion), but that's OK + int interval = + map.hasKey("interval") ? map.getInt("interval") : 10000; + int fastestInterval = + map.hasKey("fastestInterval") ? map.getInt("fastestInterval") : -1; + long timeout = + map.hasKey("timeout") ? (long) map.getDouble("timeout") : 1000 * 60 * 10; + double maximumAge = + map.hasKey("maximumAge") ? map.getDouble("maximumAge") : Double.POSITIVE_INFINITY; + boolean highAccuracy = + map.hasKey("enableHighAccuracy") && map.getBoolean("enableHighAccuracy"); + float distanceFilter = map.hasKey("distanceFilter") ? + (float) map.getDouble("distanceFilter") : + RCT_DEFAULT_LOCATION_ACCURACY; + + return new LocationOptions(interval, fastestInterval, timeout, maximumAge, highAccuracy, distanceFilter); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java b/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java new file mode 100644 index 0000000..60c89f2 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/geolocation/LocationService.java @@ -0,0 +1,98 @@ +package com.reactnativecommunity.geolocation; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import androidx.core.app.NotificationCompat; +import android.util.Log; + +import com.facebook.react.bridge.Callback; + +public class LocationService extends Service implements LocationHandler { + private static final String TAG = "LocationService"; + private final IBinder binder = new LocalBinder(); + private static final String CHANNEL_ID = "com.reactnativecommunity.geolocation.location_service"; + private static final int NOTIFICATION_ID = 1; + private LocationHandler mLocationHandler; + + public class LocalBinder extends Binder { + LocationService getService() { + return LocationService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "onCreate"); + createNotificationChannel(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.i(TAG, "onDestroy"); + stopLocationUpdates(); // Clean up resources + } + + // will be called when `startService(Intent)` being called + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(TAG, "onStartCommand"); + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Location Service") + .setContentText("Running") + .setSmallIcon(android.R.drawable.ic_menu_mylocation) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .build(); + + startForeground(NOTIFICATION_ID, notification); + + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + public void setLocationHandler(LocationHandler locationHandler) { + this.mLocationHandler = locationHandler; + } + + public void startLocationUpdates(LocationOptions options) { + Log.i(TAG, "startLocationUpdates"); + mLocationHandler.startLocationUpdates(options); + } + + public void stopLocationUpdates() { + Log.i(TAG, "stopLocationUpdates"); + mLocationHandler.stopLocationUpdates(); + } + + public void getCurrentLocation(LocationOptions options, final Callback success, + Callback error) { + Log.i(TAG, "getCurrentLocation"); + this.mLocationHandler.getCurrentLocation(options, success, error); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // 1. create a channel + NotificationChannel serviceChannel = new NotificationChannel( + CHANNEL_ID, + "GDL Location Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ); + + // 2. register the notification channel + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(serviceChannel); + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java b/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java index 423f7a5..a65758c 100644 --- a/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java +++ b/android/src/main/java/com/reactnativecommunity/geolocation/PlayServicesLocationManager.java @@ -2,7 +2,12 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; import android.location.Location; +import android.os.Build; +import android.os.IBinder; import android.os.Looper; import android.util.Log; import android.content.Context; @@ -26,163 +31,112 @@ import com.google.android.gms.location.Priority; import com.google.android.gms.location.SettingsClient; +import javax.annotation.Nullable; + @SuppressLint("MissingPermission") public class PlayServicesLocationManager extends BaseLocationManager { - private FusedLocationProviderClient mFusedLocationClient; - private LocationCallback mLocationCallback; - private LocationCallback mSingleLocationCallback; - private SettingsClient mLocationServicesSettingsClient; - - protected PlayServicesLocationManager(ReactApplicationContext reactContext) { - super(reactContext); - mFusedLocationClient = LocationServices.getFusedLocationProviderClient(reactContext); - mLocationServicesSettingsClient = LocationServices.getSettingsClient(reactContext); - } + private boolean mIsServiceRunning = false; + private @Nullable LocationHandler mLocationHandler; - @Override - public void getCurrentLocationData(ReadableMap options, Callback success, Callback error) { - AndroidLocationManager.LocationOptions locationOptions = AndroidLocationManager.LocationOptions.fromReactMap(options); - - Activity currentActivity = mReactContext.getCurrentActivity(); + private final ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + LocationService.LocalBinder binder = (LocationService.LocalBinder) service; + mLocationHandler = binder.getService(); + final PlayServicesLocationManager.LocationHandlerImpl locationHandlerImpl = + new PlayServicesLocationManager.LocationHandlerImpl( + (LocationService) mLocationHandler, + mReactContext.getCurrentActivity(), + PlayServicesLocationManager.this); + ((LocationService) mLocationHandler).setLocationHandler(locationHandlerImpl); + mIsServiceRunning = true; + } - if (currentActivity == null) { - mSingleLocationCallback = createSingleLocationCallback(success, error); - checkLocationSettings(options, mSingleLocationCallback, error); - return; + @Override + public void onServiceDisconnected(ComponentName arg0) { + mLocationHandler = null; + mIsServiceRunning = false; } + }; - try { - mFusedLocationClient.getLastLocation() - .addOnSuccessListener(currentActivity, location -> { - if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) { - success.invoke(locationToMap(location)); - } else { - mSingleLocationCallback = createSingleLocationCallback(success, error); - checkLocationSettings(options, mSingleLocationCallback, error); - } - }); - } catch (SecurityException e) { - throw e; + protected PlayServicesLocationManager(ReactApplicationContext reactContext, boolean enableBackgroundLocationUpdates) { + super(reactContext, enableBackgroundLocationUpdates); + Log.d("PlayServicesLocationManager", "enableBackgroundLocationUpdates:" + enableBackgroundLocationUpdates); + + if (enableBackgroundLocationUpdates) { + startService(); + } else { + mLocationHandler = new LocationHandlerImpl(reactContext, mReactContext.getCurrentActivity(), this); } } @Override - public void startObserving(ReadableMap options) { - mLocationCallback = new LocationCallback() { - @Override - public void onLocationResult(LocationResult locationResult) { - if (locationResult == null) { - emitError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/observer)."); - return; - } - - mReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("geolocationDidChange", locationToMap(locationResult.getLastLocation())); - } + public void getCurrentLocationData(ReadableMap options, Callback success, Callback error) { + LocationOptions locationOptions = LocationOptions.fromReactMap(options); - @Override - public void onLocationAvailability(LocationAvailability locationAvailability) { - if (!locationAvailability.isLocationAvailable()) { - emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider)."); + try { + if (mIsServiceRunning) { + if (!mEnableBackgroundLocationUpdates) { + stopService(); + mLocationHandler = new LocationHandlerImpl(mReactContext, mReactContext.getCurrentActivity(), this); + } else if (mLocationHandler == null) { + mLocationHandler = new LocationHandlerImpl(mReactContext, mReactContext.getCurrentActivity(), this); + } + } else { + if (mEnableBackgroundLocationUpdates) { + startService(); } } - }; - checkLocationSettings(options, mLocationCallback, null); - } - - @Override - public void stopObserving() { - if(mLocationCallback == null) { - return; + mLocationHandler.getCurrentLocation(locationOptions, success, error); + } catch (SecurityException e) { + throw e; } - mFusedLocationClient.removeLocationUpdates(mLocationCallback); } - private void checkLocationSettings(ReadableMap options, LocationCallback locationCallback, Callback error) { + @Override + public void startObserving(ReadableMap options) { LocationOptions locationOptions = LocationOptions.fromReactMap(options); - LocationRequest.Builder requestBuilder = new LocationRequest.Builder(locationOptions.interval); - requestBuilder.setPriority(locationOptions.highAccuracy ? Priority.PRIORITY_HIGH_ACCURACY : Priority.PRIORITY_LOW_POWER); - requestBuilder.setMaxUpdateAgeMillis((long) locationOptions.maximumAge); - - if (locationOptions.fastestInterval >= 0) { - requestBuilder.setMinUpdateIntervalMillis(locationOptions.fastestInterval); - } - - if (locationOptions.distanceFilter >= 0) { - requestBuilder.setMinUpdateDistanceMeters(locationOptions.distanceFilter); - } - LocationRequest locationRequest = requestBuilder.build(); - - LocationSettingsRequest.Builder settingsBuilder = new LocationSettingsRequest.Builder(); - settingsBuilder.addLocationRequest(locationRequest); - LocationSettingsRequest locationSettingsRequest = settingsBuilder.build(); - mLocationServicesSettingsClient.checkLocationSettings(locationSettingsRequest) - .addOnSuccessListener(locationSettingsResponse -> requestLocationUpdates(locationRequest, locationCallback)) - .addOnFailureListener(err -> { - if(isAnyProviderAvailable()){ - requestLocationUpdates(locationRequest, locationCallback); - return; - } - - if (error != null) { - error.invoke( - PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).") - ); - return; - } - emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings)."); - }); + mLocationHandler.startLocationUpdates(locationOptions); } - private void requestLocationUpdates(LocationRequest locationRequest, LocationCallback locationCallback) { - try { - mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); - } catch (SecurityException e) { - throw e; - } + @Override + public void stopObserving() { + mLocationHandler.stopLocationUpdates(); } - private boolean isAnyProviderAvailable() { - if (mReactContext == null) { - return false; + private void startService() { + if (mIsServiceRunning) { + return; } - LocationManager locationManager = - (LocationManager) mReactContext.getSystemService(Context.LOCATION_SERVICE); - return locationManager != null && (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)); - } - private LocationCallback createSingleLocationCallback(Callback success, Callback error) { - final CallbackHolder callbackHolder = new CallbackHolder(success, error); + Intent intent = new Intent(mReactContext, LocationService.class); - return new LocationCallback() { - @Override - public void onLocationResult(@NonNull LocationResult locationResult) { - Location location = locationResult.getLastLocation(); - - if (location == null) { - callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/lastLocation).")); - return; - } + // Start the LocationService as a foreground service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mReactContext.startForegroundService(intent); + } else { + mReactContext.startService(intent); + } - callbackHolder.success(location); + mReactContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } - mFusedLocationClient.removeLocationUpdates(mSingleLocationCallback); - mSingleLocationCallback = null; - } + @Override + public void stopService() { + if (!mIsServiceRunning) { + return; + } - @Override - public void onLocationAvailability(@NonNull LocationAvailability locationAvailability) { - if (!locationAvailability.isLocationAvailable()) { - callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/lastLocation).")); - } - } - }; + mReactContext.unbindService(mConnection); + Intent intent = new Intent(mReactContext, LocationService.class); + mReactContext.stopService(intent); } private static class CallbackHolder { Callback success; Callback error; + public CallbackHolder(Callback success, Callback error) { this.success = success; this.error = error; @@ -206,4 +160,156 @@ public void success(Location location) { this.success = null; } } + + private static class LocationHandlerImpl implements LocationHandler { + private static final String TAG = "PlayServicesLocationHandlerImpl"; + private final Context mContext; + private final Activity mActivity; + private FusedLocationProviderClient mFusedLocationClient; + private SettingsClient mLocationServicesSettingsClient; + private LocationCallback mSingleLocationCallback; + private LocationCallback mLocationCallback; + private final EventEmitter mEventEmitter; + + public LocationHandlerImpl(Context context, Activity activity, EventEmitter errorEmitter) { + mContext = context; + mActivity = activity; + mEventEmitter = errorEmitter; + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(context); + mLocationServicesSettingsClient = LocationServices.getSettingsClient(context); + } + + public void startLocationUpdates(LocationOptions options) { + mLocationCallback = new LocationCallback() { + @Override + public void onLocationResult(LocationResult locationResult) { + if (locationResult == null) { + mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/observer)."); + return; + } + + mEventEmitter.emit("geolocationDidChange", locationToMap(locationResult.getLastLocation())); + } + + @Override + public void onLocationAvailability(LocationAvailability locationAvailability) { + if (!locationAvailability.isLocationAvailable()) { + mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider)."); + } + } + }; + + checkLocationSettings(options, mLocationCallback, null); + } + + public void stopLocationUpdates() { + if (mLocationCallback == null) { + return; + } + mFusedLocationClient.removeLocationUpdates(mLocationCallback); + } + + public void getCurrentLocation(LocationOptions locationOptions, final Callback success, + Callback error) { + if (mActivity == null) { + mSingleLocationCallback = createSingleLocationCallback(success, error); + checkLocationSettings(locationOptions, mSingleLocationCallback, error); + return; + } + + try { + mFusedLocationClient.getLastLocation() + .addOnSuccessListener(mActivity, location -> { + if (location != null && (SystemClock.currentTimeMillis() - location.getTime()) < locationOptions.maximumAge) { + success.invoke(locationToMap(location)); + } else { + mSingleLocationCallback = createSingleLocationCallback(success, error); + checkLocationSettings(locationOptions, mSingleLocationCallback, error); + } + }); + } catch (SecurityException e) { + throw e; + } + } + + private LocationCallback createSingleLocationCallback(Callback success, Callback error) { + final CallbackHolder callbackHolder = new CallbackHolder(success, error); + + return new LocationCallback() { + @Override + public void onLocationResult(@NonNull LocationResult locationResult) { + Location location = locationResult.getLastLocation(); + + if (location == null) { + callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "No location provided (FusedLocationProvider/lastLocation).")); + return; + } + + callbackHolder.success(location); + + mFusedLocationClient.removeLocationUpdates(mSingleLocationCallback); + mSingleLocationCallback = null; + } + + @Override + public void onLocationAvailability(@NonNull LocationAvailability locationAvailability) { + if (!locationAvailability.isLocationAvailable()) { + callbackHolder.error(PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/lastLocation).")); + } + } + }; + } + + private void requestLocationUpdates(LocationRequest locationRequest, LocationCallback locationCallback) { + try { + mFusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper()); + } catch (SecurityException e) { + throw e; + } + } + + private void checkLocationSettings(LocationOptions locationOptions, LocationCallback locationCallback, Callback error) { + LocationRequest.Builder requestBuilder = new LocationRequest.Builder(locationOptions.interval); + requestBuilder.setPriority(locationOptions.highAccuracy ? Priority.PRIORITY_HIGH_ACCURACY : Priority.PRIORITY_LOW_POWER); + requestBuilder.setMaxUpdateAgeMillis((long) locationOptions.maximumAge); + + if (locationOptions.fastestInterval >= 0) { + requestBuilder.setMinUpdateIntervalMillis(locationOptions.fastestInterval); + } + + if (locationOptions.distanceFilter >= 0) { + requestBuilder.setMinUpdateDistanceMeters(locationOptions.distanceFilter); + } + LocationRequest locationRequest = requestBuilder.build(); + + LocationSettingsRequest.Builder settingsBuilder = new LocationSettingsRequest.Builder(); + settingsBuilder.addLocationRequest(locationRequest); + LocationSettingsRequest locationSettingsRequest = settingsBuilder.build(); + mLocationServicesSettingsClient.checkLocationSettings(locationSettingsRequest) + .addOnSuccessListener(locationSettingsResponse -> requestLocationUpdates(locationRequest, locationCallback)) + .addOnFailureListener(err -> { + if (isAnyProviderAvailable()) { + requestLocationUpdates(locationRequest, locationCallback); + return; + } + + if (error != null) { + error.invoke( + PositionError.buildError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings).") + ); + return; + } + mEventEmitter.emitError(PositionError.POSITION_UNAVAILABLE, "Location not available (FusedLocationProvider/settings)."); + }); + } + + private boolean isAnyProviderAvailable() { + if (mContext == null) { + return false; + } + LocationManager locationManager = + (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + return locationManager != null && (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)); + } + } } diff --git a/ios/RNCGeolocation.mm b/ios/RNCGeolocation.mm index 03df64d..e50d6b4 100644 --- a/ios/RNCGeolocation.mm +++ b/ios/RNCGeolocation.mm @@ -64,7 +64,8 @@ + (RNCGeolocationConfiguration)RNCGeolocationConfiguration:(id)json return (RNCGeolocationConfiguration) { .skipPermissionRequests = [RCTConvert BOOL:options[@"skipPermissionRequests"]], - .authorizationLevel = [RCTConvert RNCGeolocationAuthorizationLevel:options[@"authorizationLevel"]] + .authorizationLevel = [RCTConvert RNCGeolocationAuthorizationLevel:options[@"authorizationLevel"]], + .enableBackgroundLocationUpdates = [RCTConvert BOOL:options[@"enableBackgroundLocationUpdates"]] }; } @@ -173,17 +174,17 @@ - (dispatch_queue_t)methodQueue - (void)beginLocationUpdatesWithDesiredAccuracy:(CLLocationAccuracy)desiredAccuracy distanceFilter:(CLLocationDistance)distanceFilter useSignificantChanges:(BOOL)useSignificantChanges { + if (!_locationManager) { + _locationManager = [CLLocationManager new]; + _locationManager.delegate = self; + } + if (!_locationConfiguration.skipPermissionRequests) { [self requestAuthorization:nil error:nil]; } else if (_locationConfiguration.enableBackgroundLocationUpdates) { [self enableBackgroundLocationUpdates]; } - if (!_locationManager) { - _locationManager = [CLLocationManager new]; - _locationManager.delegate = self; - } - _locationManager.distanceFilter = distanceFilter; _locationManager.desiredAccuracy = desiredAccuracy; _usingSignificantChanges = useSignificantChanges; @@ -298,9 +299,7 @@ - (void)enableBackgroundLocationUpdates // iOS 9+ requires explicitly enabling background updates NSArray *backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; if (backgroundModes && [backgroundModes containsObject:@"location"]) { - if ([_locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) { - [_locationManager setAllowsBackgroundLocationUpdates:YES]; - } + _locationManager.allowsBackgroundLocationUpdates = YES; } #endif } From 1bcd4c729b9b28231cb3dae90a5a8ac17e78ec65 Mon Sep 17 00:00:00 2001 From: quan118 Date: Wed, 25 Sep 2024 15:56:30 +0700 Subject: [PATCH 2/2] Update README --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a37550..8ce82cc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ The Geolocation API 📍 module for React Native that extends the [Geolocation web spec](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation). +**This fork adds support for background location updates, allowing your app to receive location updates even when it's not in the foreground.** + Supports TurboModules ⚡️ and legacy React Native architecture. Fully compatible with TypeScript. @@ -32,11 +34,15 @@ Supports modern [Play Services Location API](https://developers.google.com/andro ## Getting started -`yarn add @react-native-community/geolocation` +This is a fork of the original `@react-native-community/geolocation` package. + +To install this forked version: + +`yarn add https://github.com/quan118/react-native-geolocation` or -`npm install @react-native-community/geolocation --save` +`npm install https://github.com/quan118/react-native-geolocationn --save` ## Configuration and Permissions @@ -70,6 +76,15 @@ or `` + +To enable background location updates, add the following permissions: + +```xml + + + +``` + Android API >= 18 Positions will also contain a `mocked` boolean to indicate if position was created from a mock provider.

@@ -181,7 +196,7 @@ Supported options: * `skipPermissionRequests` (boolean) - Defaults to `false`. If `true`, you must request permissions before using Geolocation APIs. * `authorizationLevel` (string, iOS-only) - Either `"whenInUse"`, `"always"`, or `"auto"`. Changes whether the user will be asked to give "always" or "when in use" location services permission. Any other value or `auto` will use the default behaviour, where the permission level is based on the contents of your `Info.plist`. -* `enableBackgroundLocationUpdates` (boolean, iOS-only) - When using `skipPermissionRequests`, toggle wether to automatically enableBackgroundLocationUpdates. Defaults to true. +* `enableBackgroundLocationUpdates` (boolean) - When using `skipPermissionRequests`, toggle wether to automatically enableBackgroundLocationUpdates. Defaults to true. * `locationProvider` (string, Android-only) - Either `"playServices"`, `"android"`, or `"auto"`. Determines wether to use `Google’s Location Services API` or `Android’s Location API`. The `"auto"` mode defaults to `android`, and falls back to Android's Location API if play services aren't available. ---