diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 2cf2922..9897398 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
diff --git a/android/src/main/java/com/eddieowens/services/BoundaryEventHeadlessTaskService.java b/android/src/main/java/com/eddieowens/services/BoundaryEventHeadlessTaskService.java
index 99ffa98..41faa64 100644
--- a/android/src/main/java/com/eddieowens/services/BoundaryEventHeadlessTaskService.java
+++ b/android/src/main/java/com/eddieowens/services/BoundaryEventHeadlessTaskService.java
@@ -1,21 +1,119 @@
package com.eddieowens.services;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
-import android.support.annotation.Nullable;
+import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+
+import com.eddieowens.R;
import com.facebook.react.HeadlessJsTaskService;
-import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.facebook.react.bridge.Arguments;
+import com.facebook.react.jstasks.HeadlessJsTaskConfig;
+
+import java.util.Map;
+import java.util.Random;
+
+import static com.eddieowens.RNBoundaryModule.TAG;
public class BoundaryEventHeadlessTaskService extends HeadlessJsTaskService {
+ public static final String NOTIFICATION_CHANNEL_ID = "com.eddieowens.GEOFENCE_SERVICE_CHANNEL";
+ private static final String KEY_NOTIFICATION_TITLE = "rnboundary.notification_title";
+ private static final String KEY_NOTIFICATION_TEXT = "rnboundary.notification_text";
+ private static final String KEY_NOTIFICATION_ICON = "rnboundary.notification_icon";
+
@Nullable
protected HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Bundle extras = intent.getExtras();
return new HeadlessJsTaskConfig(
- "OnBoundaryEvent",
- extras != null ? Arguments.fromBundle(extras) : null,
- 5000,
- true);
+ "OnBoundaryEvent",
+ extras != null ? Arguments.fromBundle(extras) : null,
+ 5000,
+ true);
+ }
+
+ public NotificationCompat.Builder getNotificationBuilder() {
+ Context context = getApplicationContext();
+ String title = "Geofencing in progress";
+ String text = "You're close to the configured location";
+ int iconResource = -1;
+
+ try {
+ ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ Bundle bundle = ai.metaData;
+ title = bundle.getString(KEY_NOTIFICATION_TITLE, title);
+ text = bundle.getString(KEY_NOTIFICATION_TEXT, text);
+ iconResource = bundle.getInt(KEY_NOTIFICATION_ICON, -1);
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot get application Bundle " + e.toString());
+ }
+
+
+ // Notification for the foreground service
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.accent_material_light));
+
+ if (iconResource > -1) {
+ builder.setSmallIcon(iconResource);
+ }
+
+ return builder;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ startForegroundServiceNotification();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ int result = super.onStartCommand(intent, flags, startId);
+ startForegroundServiceNotification();
+ return result;
+ }
+
+ private void startForegroundServiceNotification() {
+ Context context = this.getApplicationContext();
+
+ // Channel for the foreground service notification
+ createChannel(context);
+
+ NotificationCompat.Builder builder = getNotificationBuilder();
+ Notification notification = builder.build();
+
+ Random rand = new Random();
+ int notificationId = rand.nextInt(100000);
+
+ startForeground(notificationId, notification);
+ HeadlessJsTaskService.acquireWakeLockNow(context);
+ }
+
+ private void createChannel(Context context) {
+ String NOTIFICATION_CHANNEL_NAME = "Geofence Service";
+ String NOTIFICATION_CHANNEL_DESCRIPTION = "Only used to know when you're close to a configured location.";
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ NOTIFICATION_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW);
+ channel.setDescription(NOTIFICATION_CHANNEL_DESCRIPTION);
+
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
}
}
diff --git a/android/src/main/java/com/eddieowens/services/BoundaryEventJobIntentService.java b/android/src/main/java/com/eddieowens/services/BoundaryEventJobIntentService.java
index 3997859..4454417 100644
--- a/android/src/main/java/com/eddieowens/services/BoundaryEventJobIntentService.java
+++ b/android/src/main/java/com/eddieowens/services/BoundaryEventJobIntentService.java
@@ -1,12 +1,15 @@
package com.eddieowens.services;
+import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
+import android.os.Build;
import android.os.Bundle;
-import android.support.annotation.NonNull;
-import android.support.v4.app.JobIntentService;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.core.app.JobIntentService;
+
import com.eddieowens.RNBoundaryModule;
import com.eddieowens.errors.GeofenceErrorMessages;
import com.facebook.react.HeadlessJsTaskService;
@@ -14,6 +17,7 @@
import com.google.android.gms.location.GeofencingEvent;
import java.util.ArrayList;
+import java.util.List;
import static com.eddieowens.RNBoundaryModule.TAG;
@@ -68,7 +72,33 @@ private void sendEvent(Context context, String event, ArrayList params)
Intent headlessBoundaryIntent = new Intent(context, BoundaryEventHeadlessTaskService.class);
headlessBoundaryIntent.putExtras(bundle);
- context.startService(headlessBoundaryIntent);
- HeadlessJsTaskService.acquireWakeLockNow(context);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || isAppOnForeground(context)) {
+ context.startService(headlessBoundaryIntent);
+ HeadlessJsTaskService.acquireWakeLockNow(context);
+ }
+ else {
+ // Since Oreo (8.0) and up they have restricted starting background services, and it will crash the app
+ // But we can create a foreground service and bring an notification to the front
+ // http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
+ context.startForegroundService(headlessBoundaryIntent);
+ }
+ }
+
+ private boolean isAppOnForeground(Context context) {
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List appProcesses =
+ activityManager.getRunningAppProcesses();
+ if (appProcesses == null) {
+ return false;
+ }
+ final String packageName = context.getPackageName();
+ for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
+ if (appProcess.importance ==
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
+ appProcess.processName.equals(packageName)) {
+ return true;
+ }
+ }
+ return false;
}
}
\ No newline at end of file
diff --git a/ios/RNBoundary.h b/ios/RNBoundary.h
index 12b5e82..cbba3d3 100644
--- a/ios/RNBoundary.h
+++ b/ios/RNBoundary.h
@@ -12,5 +12,14 @@
- (bool) removeBoundary:(NSString *)boundaryId;
- (void) removeAllBoundaries;
@property (strong, nonatomic) CLLocationManager *locationManager;
+@property (strong, nonatomic) NSMutableSet *queuedEvents;
+@property (assign, nonatomic) bool hasListeners;
+@end
+
+
+@interface GeofenceEvent : NSObject
+- (id) initWithId:(NSString*)geofenceId forEvent:(NSString *)eventName;
+@property (strong, nonatomic) NSString *geofenceId;
+@property (strong, nonatomic) NSString *name;
+@property (strong, nonatomic) NSDate* date;
@end
-
diff --git a/ios/RNBoundary.m b/ios/RNBoundary.m
index 4e10084..3abb040 100644
--- a/ios/RNBoundary.m
+++ b/ios/RNBoundary.m
@@ -1,6 +1,29 @@
-
#import "RNBoundary.h"
+@implementation GeofenceEvent
+- (id)initWithId:(NSString*)geofenceId forEvent:(NSString *)name {
+ self = [super init];
+ if (self) {
+ self.geofenceId = geofenceId;
+ self.name = name;
+ self.date = [NSDate date];
+ }
+
+ return self;
+}
+
+- (BOOL)isEqual:(id)anObject
+{
+ return [self.geofenceId isEqual:((GeofenceEvent *)anObject).geofenceId];
+}
+
+- (NSUInteger)hash
+{
+ return self.geofenceId;
+}
+@end
+
+
@implementation RNBoundary
RCT_EXPORT_MODULE()
@@ -11,6 +34,8 @@ -(instancetype)init
if (self) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
+
+ self.queuedEvents = [[NSMutableSet alloc] init];
}
return self;
@@ -62,6 +87,7 @@ - (void) removeAllBoundaries
for(CLRegion *region in [self.locationManager monitoredRegions]) {
[self.locationManager stopMonitoringForRegion:region];
}
+ [self.queuedEvents removeAllObjects];
}
- (bool) removeBoundary:(NSString *)boundaryId
@@ -83,13 +109,49 @@ - (bool) removeBoundary:(NSString *)boundaryId
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
NSLog(@"didEnter : %@", region);
- [self sendEventWithName:@"onEnter" body:region.identifier];
+ if (self.hasListeners) {
+ [self sendEventWithName:@"onEnter" body:region.identifier];
+ } else {
+ GeofenceEvent *event = [[GeofenceEvent alloc] initWithId:region.identifier forEvent:@"onEnter" ];
+ [self.queuedEvents addObject:event];
+ }
}
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region
{
NSLog(@"didExit : %@", region);
- [self sendEventWithName:@"onExit" body:region.identifier];
+ if (self.hasListeners) {
+ [self sendEventWithName:@"onExit" body:region.identifier];
+ } else {
+ GeofenceEvent *event = [[GeofenceEvent alloc] initWithId:region.identifier forEvent:@"onExit" ];
+ [self.queuedEvents addObject:event];
+ }
+}
+
+- (void)startObserving {
+ self.hasListeners = YES;
+ if ([self.queuedEvents count] > 0) {
+ for(GeofenceEvent *event in self.queuedEvents) {
+ NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:[event date]];
+ double minutesDiff = interval / 60.f;
+ // if the app was not open
+ // within 2 minutes of storing the event
+ // we discard it
+ if (minutesDiff < 2) {
+ // dispatch after 1 second
+ // as both events are not registered at the same time
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [self sendEventWithName:[event name] body:[event geofenceId]];
+ });
+ }
+ }
+ [self.queuedEvents removeAllObjects];
+ }
+}
+
+- (void)stopObserving {
+ self.hasListeners = NO;
+ [self.queuedEvents removeAllObjects];
}
+ (BOOL)requiresMainQueueSetup
@@ -98,4 +160,3 @@ + (BOOL)requiresMainQueueSetup
}
@end
-
diff --git a/ios/RNBoundary.podspec b/ios/RNBoundary.podspec
index 3587945..056e951 100644
--- a/ios/RNBoundary.podspec
+++ b/ios/RNBoundary.podspec
@@ -1,24 +1,20 @@
-
-Pod::Spec.new do |s|
- s.name = "RNBoundary"
- s.version = "1.0.0"
- s.summary = "RNBoundary"
- s.description = <<-DESC
- RNBoundary
- DESC
- s.homepage = ""
- s.license = "MIT"
- # s.license = { :type => "MIT", :file => "FILE_LICENSE" }
- s.author = { "author" => "author@domain.cn" }
- s.platform = :ios, "7.0"
- s.source = { :git => "https://github.com/author/RNBoundary.git", :tag => "master" }
- s.source_files = "RNBoundary/**/*.{h,m}"
- s.requires_arc = true
-
-
- s.dependency "React"
- #s.dependency "others"
-
-end
-
-
\ No newline at end of file
+require 'json'
+
+package = JSON.parse(File.read(File.join(__dir__, '../package.json')))
+
+Pod::Spec.new do |s|
+ s.name = "RNBoundary"
+ s.version = package['version']
+ s.summary = package['description']
+ s.license = package['license']
+
+ s.authors = package['author']
+ s.homepage = "https://github.com/eddieowens/react-native-boundary#readme"
+ s.platform = :ios, "9.0"
+
+ s.source = { :git => "https://github.com/eddieowens/react-native-boundary.git", :tag => "#{s.version}" }
+ s.source_files = "*.{h,m}"
+ s.requires_arc = true
+
+ s.dependency 'React'
+end
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..450736e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,40 @@
+{
+ "name": "react-native-boundary-woffu",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/prop-types": {
+ "version": "15.7.1",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz",
+ "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==",
+ "dev": true
+ },
+ "@types/react": {
+ "version": "16.9.1",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.1.tgz",
+ "integrity": "sha512-jGM2x8F7m7/r+81N/BOaUKVwbC5Cdw6ExlWEUpr77XPwVeNvAppnPEnMMLMfxRDYL8FPEX8MHjwtD2NQMJ0yyQ==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "csstype": "^2.2.0"
+ }
+ },
+ "@types/react-native": {
+ "version": "0.57.65",
+ "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.57.65.tgz",
+ "integrity": "sha512-7P5ulTb+/cnwbABWaAjzKmSYkRWeK7UCTfUwHhDpnwxdiL2X/KbdN1sPgo0B2E4zxfYE3MEoHv7FhB8Acfvf8A==",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/react": "*"
+ }
+ },
+ "csstype": {
+ "version": "2.6.6",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz",
+ "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index 5f3b574..a88928e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-boundary",
- "version": "1.1.1",
+ "version": "1.2.0",
"description": "native geofencing and region monitoring",
"main": "index.js",
"scripts": {