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": {