From 0fe670fbc6f796f44f0cbf5de02069f215c645b2 Mon Sep 17 00:00:00 2001 From: Manuel Adameit Date: Sat, 30 Jun 2018 12:07:53 +0200 Subject: [PATCH 1/6] Implement geofencing for ios Add geofence example tab Improve geofence data structure Show errors in geofence tab Request location permission when listening Check for exact permission. TODO: strange behavior for other use cases Implement removing adding and listing of geofence regions in dart, ios --- example/ios/Podfile.lock | 6 +- example/ios/Runner.xcodeproj/project.pbxproj | 18 -- example/lib/main.dart | 10 +- example/lib/tab_geofence.dart | 277 +++++++++++++++++++ ios/Classes/Codec.swift | 8 +- ios/Classes/Data/Location.swift | 12 +- ios/Classes/Data/Permission.swift | 19 ++ ios/Classes/Data/Region.swift | 52 ++++ ios/Classes/Location/LocationClient.swift | 75 ++++- ios/Classes/LocationChannel.swift | 60 +++- lib/channel/codec.dart | 72 ++++- lib/channel/geofence_channel.dart | 36 +++ lib/channel/location_channel.dart | 7 +- lib/data/location.dart | 3 +- lib/data/region.dart | 59 ++++ lib/geolocation.dart | 29 +- 16 files changed, 696 insertions(+), 47 deletions(-) create mode 100644 example/lib/tab_geofence.dart create mode 100644 ios/Classes/Data/Region.swift create mode 100644 lib/channel/geofence_channel.dart create mode 100644 lib/data/region.dart diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1fe1d2f..4265e82 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -14,9 +14,9 @@ EXTERNAL SOURCES: :path: Pods/.symlinks/plugins/geolocation/ios SPEC CHECKSUMS: - Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a - geolocation: b696ce6d0af163815dacc7b9c2a9719d61f42210 + Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 + geolocation: 3bc7405d8efd3e757b52ce85820cb613abd896cb PODFILE CHECKSUM: c2762ce8317a0e5ab011483b2ae931fa1509d6a6 -COCOAPODS: 1.4.0.beta.2 +COCOAPODS: 1.5.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 43d9497..6f6a50d 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -158,7 +158,6 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, FD6890096B8B677EA261C05B /* [CP] Embed Pods Frameworks */, - 1A986EC08373F667C99A13EB /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -220,21 +219,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1A986EC08373F667C99A13EB /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -288,12 +272,10 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/.symlinks/flutter/ios/Flutter.framework", "${BUILT_PRODUCTS_DIR}/geolocation/geolocation.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocation.framework", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/example/lib/main.dart b/example/lib/main.dart index 28f34d3..d78b171 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:geolocation/geolocation.dart'; +import 'package:geolocation_example/tab_geofence.dart'; import 'tab_location.dart'; import 'tab_track.dart'; import 'tab_settings.dart'; @@ -25,6 +26,7 @@ class _MyAppState extends State { return new MaterialApp( home: new CupertinoTabScaffold( tabBar: new CupertinoTabBar( + currentIndex: 2, items: [ new BottomNavigationBarItem( title: new Text('Current'), @@ -34,6 +36,10 @@ class _MyAppState extends State { title: new Text('Track'), icon: new Icon(Icons.location_searching), ), + new BottomNavigationBarItem( + title: new Text('Geofence'), + icon: new Icon(Icons.filter_tilt_shift), + ), new BottomNavigationBarItem( title: new Text('Geocode'), icon: new Icon(Icons.location_city), @@ -52,7 +58,9 @@ class _MyAppState extends State { return new TabLocation(); case 1: return new TabTrack(); - case 3: + case 2: + return new TabGeofence(); + case 4: return new TabSettings(); default: return new Container( diff --git a/example/lib/tab_geofence.dart b/example/lib/tab_geofence.dart new file mode 100644 index 0000000..2a111b1 --- /dev/null +++ b/example/lib/tab_geofence.dart @@ -0,0 +1,277 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:geolocation/geolocation.dart'; + +class TabGeofence extends StatefulWidget { + @override + _TabGeofenceState createState() => new _TabGeofenceState(); +} + +class _TabGeofenceState extends State { + List _geofenceEventResults = []; + StreamSubscription _subscription; + bool _isTracking = false; + + @override + dispose() { + super.dispose(); + _subscription.cancel(); + } + + _onTogglePressed() { + if (_isTracking) { + setState(() { + _isTracking = false; + }); + + _subscription.cancel(); + _subscription = null; + } else { + setState(() { + _isTracking = true; + }); + + _subscription = Geolocation.geofenceUpdates.listen((result) { + setState(() { + _geofenceEventResults.insert(0, result); + }); + }); + + _subscription.onDone(() { + setState(() { + _isTracking = false; + }); + }); + } + } + + _onAddPressed() async { + final region = GeofenceRegion( + id: "MyHome", + notifyOnExit: true, + notifyOnEntry: true, + region: Region( + center: Location(longitude: 10.0, latitude: 10.0), radius: 10.0)); + Geolocation.addGeofenceRegion(region); + print(await Geolocation.geofenceRegions()); + } + + _onRemovePressed() async { + final region = GeofenceRegion( + id: "MyHome", + notifyOnExit: true, + notifyOnEntry: true, + region: Region( + center: Location(longitude: 10.0, latitude: 10.0), radius: 10.0)); + Geolocation.removeGeofenceRegion(region); + print(await Geolocation.geofenceRegions()); + } + + @override + Widget build(BuildContext context) { + List children = [ + new _Header( + isRunning: _isTracking, + onTogglePressed: _onTogglePressed, + onAddPressed: _onAddPressed, + onRemovePressed: _onRemovePressed, + ) + ]; + + children.addAll(ListTile.divideTiles( + context: context, + tiles: _geofenceEventResults + .map((location) => new _Item(data: location)) + .toList(), + )); + + return new Scaffold( + appBar: new AppBar( + title: new Text('Geofence updates'), + ), + body: new ListView( + children: children, + ), + ); + } +} + +class _Header extends StatelessWidget { + _Header( + {@required this.isRunning, + @required this.onTogglePressed, + @required this.onAddPressed, + @required this.onRemovePressed}); + + final bool isRunning; + final VoidCallback onTogglePressed; + final VoidCallback onAddPressed; + final VoidCallback onRemovePressed; + + @override + Widget build(BuildContext context) { + return new Padding( + padding: const EdgeInsets.symmetric(vertical: 20.0), + child: new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new _HeaderButton( + title: isRunning ? 'Stop' : 'Start', + color: isRunning ? Colors.deepOrange : Colors.teal, + onTap: onTogglePressed, + ), + new _HeaderButton( + title: 'Add Region', + color: Colors.lightGreen, + onTap: onAddPressed, + ), + new _HeaderButton( + title: 'Remove Region', + color: Colors.deepOrange, + onTap: onRemovePressed, + ), + ], + ), + ], + ), + ); + } +} + +class _HeaderButton extends StatelessWidget { + _HeaderButton( + {@required this.title, @required this.color, @required this.onTap}); + + final String title; + final Color color; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return new Padding( + padding: const EdgeInsets.symmetric(horizontal: 6.0), + child: new GestureDetector( + onTap: onTap, + child: new Container( + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0), + decoration: new BoxDecoration( + color: color, + borderRadius: new BorderRadius.all( + new Radius.circular(6.0), + ), + ), + child: new Text( + title, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ); + } +} + +class _Item extends StatelessWidget { + _Item({@required this.data}); + + final GeofenceEventResult data; + + @override + Widget build(BuildContext context) { + String text; + String status; + Color color; + + if (data.isSuccessful) { + String locationName = data.geofenceEvent.geofenceRegion.id; + String event = data.geofenceEvent.type == GeofenceEventType.entered + ? "Entered" + : "Exited"; + + text = '$event: $locationName'; + status = 'success'; + color = Colors.green; + } else { + switch (data.error.type) { + case GeolocationResultErrorType.runtime: + text = 'Failure: ${data.error.message}'; + break; + case GeolocationResultErrorType.locationNotFound: + text = 'Location not found'; + break; + case GeolocationResultErrorType.serviceDisabled: + text = 'Service disabled'; + break; + case GeolocationResultErrorType.permissionDenied: + text = 'Permission denied'; + break; + case GeolocationResultErrorType.playServicesUnavailable: + text = 'Play services unavailable: ${data.error.additionalInfo}'; + break; + } + + status = 'failure'; + color = Colors.red; + } + + final List content = [ + new Text( + text, + style: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + ]; + + return new Container( + color: Colors.white, + child: new SizedBox( + height: 56.0, + child: new Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: new Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + new Expanded( + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: content, + ), + ), + new Container( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + decoration: new BoxDecoration( + color: color, + borderRadius: new BorderRadius.circular(6.0), + ), + child: new Text( + status, + style: const TextStyle( + color: Colors.white, + fontSize: 12.0, + ), + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/ios/Classes/Codec.swift b/ios/Classes/Codec.swift index 12fa58c..24190d9 100644 --- a/ios/Classes/Codec.swift +++ b/ios/Classes/Codec.swift @@ -9,8 +9,8 @@ struct Codec { private static let jsonEncoder = JSONEncoder() private static let jsonDecoder = JSONDecoder() - static func encode(result: Result) -> String { - return String(data: try! jsonEncoder.encode(result), encoding: .utf8)! + static func encode(_ value: T) -> String where T: Encodable { + return String(data: try! jsonEncoder.encode(value), encoding: .utf8)! } static func decodeInt(from arguments: Any?) -> Int { @@ -24,4 +24,8 @@ struct Codec { static func decodeLocationUpdatesRequest(from arguments: Any?) -> LocationUpdatesRequest { return try! jsonDecoder.decode(LocationUpdatesRequest.self, from: (arguments as! String).data(using: .utf8)!) } + + static func decodeGeofenceRegion(from arguments: Any?) -> GeofenceRegion { + return try! jsonDecoder.decode(GeofenceRegion.self, from: (arguments as! String).data(using: .utf8)!) + } } diff --git a/ios/Classes/Data/Location.swift b/ios/Classes/Data/Location.swift index f68597e..55a67b0 100644 --- a/ios/Classes/Data/Location.swift +++ b/ios/Classes/Data/Location.swift @@ -10,12 +10,22 @@ struct Location : Codable { let latitude: Double let longitude: Double let altitude: Double - + init(from location: CLLocation) { self.latitude = location.coordinate.latitude self.longitude = location.coordinate.longitude self.altitude = location.altitude as Double } + + init(from coordinate: CLLocationCoordinate2D) { + self.latitude = coordinate.latitude + self.longitude = coordinate.longitude + self.altitude = 0 + } + + var coordinate2D: CLLocationCoordinate2D { + return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + } } diff --git a/ios/Classes/Data/Permission.swift b/ios/Classes/Data/Permission.swift index e253967..e6fef7c 100644 --- a/ios/Classes/Data/Permission.swift +++ b/ios/Classes/Data/Permission.swift @@ -4,8 +4,27 @@ // import Foundation +import CoreLocation enum Permission: String, Codable { case whenInUse = "whenInUse" case always = "always" } + +extension Permission { + func statusIsSufficient(_ status: CLAuthorizationStatus) -> Bool { + switch status { + case .authorizedAlways: + return true + case .authorizedWhenInUse: + switch self { + case .always: + return false + case .whenInUse: + return true + } + default: + return false + } + } +} diff --git a/ios/Classes/Data/Region.swift b/ios/Classes/Data/Region.swift new file mode 100644 index 0000000..8c27ab0 --- /dev/null +++ b/ios/Classes/Data/Region.swift @@ -0,0 +1,52 @@ +// +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 +// + +import Foundation +import CoreLocation + +struct Region: Codable { + let center: Location + let radius: Double + + init(from region: CLCircularRegion) { + self.center = Location(from: region.center) + self.radius = region.radius + } +} + +struct GeofenceRegion: Codable { + let region: Region + let id: String + let notifyOnEntry: Bool + let notifyOnExit: Bool + + init(from region: CLCircularRegion) { + self.region = Region(from: region) + self.id = region.identifier + self.notifyOnEntry = region.notifyOnEntry + self.notifyOnExit = region.notifyOnExit + } + + var clRegion: CLCircularRegion { + let result = CLCircularRegion(center: self.region.center.coordinate2D, radius: self.region.radius, identifier: self.id) + result.notifyOnExit = self.notifyOnExit + result.notifyOnEntry = self.notifyOnEntry + return result + } +} + +enum GeofenceEventType: String, Codable { + case entered, exited +} + +struct GeofenceEvent: Codable { + let type: GeofenceEventType + let geofenceRegion: GeofenceRegion + + init(region: CLCircularRegion, type: GeofenceEventType) { + self.type = type + self.geofenceRegion = GeofenceRegion(from: region) + } +} diff --git a/ios/Classes/Location/LocationClient.swift b/ios/Classes/Location/LocationClient.swift index 9b1671b..e04f968 100644 --- a/ios/Classes/Location/LocationClient.swift +++ b/ios/Classes/Location/LocationClient.swift @@ -9,7 +9,7 @@ import CoreLocation class LocationClient : NSObject, CLLocationManagerDelegate { private let locationManager = CLLocationManager() - private var permissionCallbacks: Array> = [] + private var permissionCallbacks: Array<(Permission, Callback)> = [] private var locationUpdatesCallback: LocationUpdatesCallback? = nil private var locationUpdatesRequests: Array = [] @@ -21,6 +21,8 @@ class LocationClient : NSObject, CLLocationManagerDelegate { return !locationUpdatesRequests.filter { $0.inBackground == true }.isEmpty } + private var geofenceUpdatesCallback: GeofenceUpdatesCallback? = nil + private var isPaused = false override init() { @@ -54,6 +56,34 @@ class LocationClient : NSObject, CLLocationManagerDelegate { }, failure: callback) } + // Geofencing API + + func addGeofenceRegion(_ region: GeofenceRegion) { + if CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) { + locationManager.startMonitoring(for: region.clRegion) + } else { + _ = 4 + //TODO + } + } + + func removeGeofenceRegion(_ region: GeofenceRegion) { + locationManager.stopMonitoring(for: region.clRegion) + } + + func geofenceRegions() -> [GeofenceRegion] { + return Array(locationManager.monitoredRegions).sorted(by: { $0.identifier < $1.identifier }).map({ GeofenceRegion(from: $0 as! CLCircularRegion) }) + } + + func registerGeofenceUpdates(callback: @escaping GeofenceUpdatesCallback) { + precondition(geofenceUpdatesCallback == nil, "trying to register a 2nd location geofence callback") + geofenceUpdatesCallback = callback + } + + func deregisterGeofenceUpdatesCallback() { + precondition(geofenceUpdatesCallback != nil, "trying to deregister a non-existent geofence updates callback") + geofenceUpdatesCallback = nil + } // Updates API @@ -144,7 +174,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { // Service status - private func runWithValidServiceStatus(with permission: Permission, success: @escaping () -> Void, failure: @escaping (Result) -> Void) { + func runWithValidServiceStatus(with permission: Permission, success: @escaping () -> Void, failure: @escaping (Result) -> Void) { let status: ServiceStatus = currentServiceStatus(with: permission) if status.isReady { @@ -155,7 +185,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { success: { _ in success() }, failure: { _ in failure(Result.failure(of: .permissionDenied)) } ) - permissionCallbacks.append(callback) + permissionCallbacks.append((permission, callback)) locationManager.requestAuthorization(for: permission) } else { failure(status.failure!) @@ -168,7 +198,8 @@ class LocationClient : NSObject, CLLocationManagerDelegate { return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .serviceDisabled)) } - switch CLLocationManager.authorizationStatus() { + let status = CLLocationManager.authorizationStatus() + switch status { case .notDetermined: guard locationManager.isPermissionDeclared(for: permission) else { return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .runtime, message: "Missing location usage description values in Info.plist. See readme for details.", fatal: true)) @@ -179,7 +210,13 @@ class LocationClient : NSObject, CLLocationManagerDelegate { return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .permissionDenied)) case .restricted: return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .serviceDisabled)) - case .authorizedWhenInUse, .authorizedAlways: + case .authorizedWhenInUse: + if permission.statusIsSufficient(status) { + return ServiceStatus(isReady: true, needsAuthorization: nil, failure: nil) + } else { + return ServiceStatus(isReady: false, needsAuthorization: nil, failure: Result.failure(of: .permissionDenied)) + } + case .authorizedAlways: return ServiceStatus(isReady: true, needsAuthorization: nil, failure: nil) } } @@ -188,8 +225,8 @@ class LocationClient : NSObject, CLLocationManagerDelegate { // CLLocationManagerDelegate public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - permissionCallbacks.forEach { action in - if status == .authorizedAlways || status == .authorizedWhenInUse { + permissionCallbacks.forEach { (permission, action) in + if permission.statusIsSufficient(status) { action.success(()) } else { action.failure(()) @@ -206,6 +243,28 @@ class LocationClient : NSObject, CLLocationManagerDelegate { locationUpdatesCallback?(Result<[Location]>.failure(of: .runtime, message: error.localizedDescription)) } + public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { + NSLog("didEnterRegion") + guard let region = region as? CLCircularRegion else { + NSLog("Expected circular region, ignoring event.") + return + } + + let geofenceEvent = GeofenceEvent(region: region, type: .entered) + geofenceUpdatesCallback?(Result.success(with: geofenceEvent)) + } + + public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { + NSLog("didExitRegion") + guard let region = region as? CLCircularRegion else { + NSLog("Expected circular region, ignoring event.") + return + } + + let geofenceEvent = GeofenceEvent(region: region, type: .exited) + geofenceUpdatesCallback?(Result.success(with: geofenceEvent)) + } + struct Callback { let success: (T) -> Void let failure: (E) -> Void @@ -213,6 +272,8 @@ class LocationClient : NSObject, CLLocationManagerDelegate { typealias LocationUpdatesCallback = (Result<[Location]>) -> Void + typealias GeofenceUpdatesCallback = (Result) -> Void + struct ServiceStatus { let isReady: Bool let needsAuthorization: Permission? diff --git a/ios/Classes/LocationChannel.swift b/ios/Classes/LocationChannel.swift index 7e5a0b2..47d8b42 100644 --- a/ios/Classes/LocationChannel.swift +++ b/ios/Classes/LocationChannel.swift @@ -10,18 +10,23 @@ class LocationChannel { private let locationClient: LocationClient private let locationUpdatesHandler: LocationUpdatesHandler + private let geofenceUpdatesHandler: GeofenceUpdatesHandler init(locationClient: LocationClient) { self.locationClient = locationClient self.locationUpdatesHandler = LocationUpdatesHandler(locationClient: locationClient) + self.geofenceUpdatesHandler = GeofenceUpdatesHandler(locationClient: locationClient) } func register(on plugin: SwiftGeolocationPlugin) { let methodChannel = FlutterMethodChannel(name: "geolocation/location", binaryMessenger: plugin.registrar.messenger()) methodChannel.setMethodCallHandler(handleMethodCall(_:result:)) - let eventChannel = FlutterEventChannel(name: "geolocation/locationUpdates", binaryMessenger: plugin.registrar.messenger()) - eventChannel.setStreamHandler(locationUpdatesHandler) + let locationEventChannel = FlutterEventChannel(name: "geolocation/locationUpdates", binaryMessenger: plugin.registrar.messenger()) + locationEventChannel.setStreamHandler(locationUpdatesHandler) + + let geofenceEventChannel = FlutterEventChannel(name: "geolocation/geofenceUpdates", binaryMessenger: plugin.registrar.messenger()) + geofenceEventChannel.setStreamHandler(geofenceUpdatesHandler) } private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -36,24 +41,30 @@ class LocationChannel { addLocationUpdatesRequest(Codec.decodeLocationUpdatesRequest(from: call.arguments)) case "removeLocationUpdatesRequest": removeLocationUpdatesRequest(Codec.decodeInt(from: call.arguments)) + case "addGeofenceRegion": + addGeofenceRegion(Codec.decodeGeofenceRegion(from: call.arguments)) + case "removeGeofenceRegion": + removeGeofenceRegion(Codec.decodeGeofenceRegion(from: call.arguments)) + case "geofenceRegions": + geofenceRegions(on: result) default: result(FlutterMethodNotImplemented) } } private func isLocationOperational(permission: Permission, on flutterResult: @escaping FlutterResult) { - flutterResult(Codec.encode(result: locationClient.isLocationOperational(with: permission))) + flutterResult(Codec.encode(locationClient.isLocationOperational(with: permission))) } private func requestLocationPermission(permission: Permission, on flutterResult: @escaping FlutterResult) { locationClient.requestLocationPermission(with: permission) { result in - flutterResult(Codec.encode(result: result)) + flutterResult(Codec.encode(result)) } } private func lastKnownLocation(permission: Permission, on flutterResult: @escaping FlutterResult) { locationClient.lastKnownLocation(with: permission) { result in - flutterResult(Codec.encode(result: result)) + flutterResult(Codec.encode(result)) } } @@ -65,6 +76,17 @@ class LocationChannel { locationClient.removeLocationUpdates(requestId: id) } + private func addGeofenceRegion(_ region: GeofenceRegion) { + locationClient.addGeofenceRegion(region) + } + + private func removeGeofenceRegion(_ region: GeofenceRegion) { + locationClient.removeGeofenceRegion(region) + } + + private func geofenceRegions(on flutterResult: @escaping FlutterResult) { + flutterResult(Codec.encode(locationClient.geofenceRegions())) + } class LocationUpdatesHandler: NSObject, FlutterStreamHandler { private let locationClient: LocationClient @@ -75,7 +97,7 @@ class LocationChannel { public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { locationClient.registerLocationUpdates { result in - events(Codec.encode(result: result)) + events(Codec.encode(result)) } return nil } @@ -85,4 +107,30 @@ class LocationChannel { return nil } } + + class GeofenceUpdatesHandler: NSObject, FlutterStreamHandler { + private let locationClient: LocationClient + + init(locationClient: LocationClient) { + self.locationClient = locationClient + } + + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + locationClient.registerGeofenceUpdates { result in + events(Codec.encode(result)) + } + + locationClient.runWithValidServiceStatus(with: Permission.always, success: { + }) { (result: Result) in + events(Codec.encode(result)) + } + + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + locationClient.deregisterGeofenceUpdatesCallback() + return nil + } + } } diff --git a/lib/channel/codec.dart b/lib/channel/codec.dart index 2cf1839..7337f1b 100644 --- a/lib/channel/codec.dart +++ b/lib/channel/codec.dart @@ -10,6 +10,22 @@ class _Codec { static LocationResult decodeLocationResult(String data) => _JsonCodec.locationResultFromJson(json.decode(data)); + static GeofenceEventResult decodeGeofenceEventResult(String data) => + _JsonCodec.geofenceEventResultFromJson(json.decode(data)); + + static String encodeGeofenceRegion(GeofenceRegion geofenceRegion) => + json.encode(_JsonCodec.geofenceRegionToJson(geofenceRegion)); + + static GeofenceRegion decodeGeofenceRegion(String data) => + _JsonCodec.geofenceRegionFromJson(json.decode(data)); + + static List decodeGeofenceRegions(String data) { + final List elements = json.decode(data); + return elements + .map((element) => _JsonCodec.geofenceRegionFromJson(element)) + .toList(); + } + static String encodeLocationPermission(LocationPermission permission) => platformSpecific( android: _Codec.encodeEnum(permission.android), @@ -28,6 +44,10 @@ class _Codec { return value.toString().split('.').last; } + static T decodeEnum(String data, List possibleValues) { + return possibleValues.firstWhere((v) => describeEnum(v) == data); + } + static String platformSpecific({ @required String android, @required String ios, @@ -87,12 +107,21 @@ class _JsonCodec { : null, ); - static Location locationFromJson(Map json) => new Location._( - _Codec.parseJsonNumber(json['latitude']), - _Codec.parseJsonNumber(json['longitude']), - _Codec.parseJsonNumber(json['altitude']), + static Location locationFromJson(Map json) => new Location( + latitude: _Codec.parseJsonNumber(json['latitude']), + longitude: _Codec.parseJsonNumber(json['longitude']), + altitude: _Codec.parseJsonNumber(json['altitude']), ); + static Map geofenceRegionToJson( + GeofenceRegion geofenceRegion) => + { + 'id': geofenceRegion.id, + 'region': regionToJson(geofenceRegion.region), + 'notifyOnEntry': geofenceRegion.notifyOnEntry, + 'notifyOnExit': geofenceRegion.notifyOnExit, + }; + static Map locationUpdatesRequestToJson( _LocationUpdatesRequest request) => { @@ -110,4 +139,39 @@ class _JsonCodec { ios: _Codec.encodeEnum(request.accuracy.ios), ), }; + + static Map regionToJson(Region region) => { + 'radius': region.radius, + 'center': locationToJson(region.center), + }; + + static Map locationToJson(Location location) => { + 'latitude': location.latitude, + 'longitude': location.longitude, + 'altitude': location.altitude + }; + + static GeofenceEventResult geofenceEventResultFromJson( + Map json) => + new GeofenceEventResult._( + json['isSuccessful'], + json['error'] != null ? resultErrorFromJson(json['error']) : null, + json['data'] != null ? geofenceEventFromJson(json['data']) : null); + + static GeofenceEvent geofenceEventFromJson(Map json) => + new GeofenceEvent._( + _Codec.decodeEnum(json['type'], GeofenceEventType.values), + _JsonCodec.geofenceRegionFromJson(json['geofenceRegion'])); + + static GeofenceRegion geofenceRegionFromJson(Map json) => + new GeofenceRegion( + region: _JsonCodec.regionFromJson(json['region']), + id: json['id'], + notifyOnEntry: json['notifyOnEntry'], + notifyOnExit: json['notifyOnExit'], + ); + + static Region regionFromJson(Map json) => new Region( + center: _JsonCodec.locationFromJson(json['center']), + radius: _Codec.parseJsonNumber(json['radius'])); } diff --git a/lib/channel/geofence_channel.dart b/lib/channel/geofence_channel.dart new file mode 100644 index 0000000..31734c3 --- /dev/null +++ b/lib/channel/geofence_channel.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +part of geolocation; + +class _GeofenceChannel { + final MethodChannel _channel; + + static const EventChannel _updatesChannel = + const EventChannel('geolocation/geofenceUpdates'); + + static const String _loggingTag = 'geofence result'; + + final Stream _geofenceUpdates = + _updatesChannel.receiveBroadcastStream().map((data) { + _log(data, tag: _loggingTag); + return _Codec.decodeGeofenceEventResult(data); + }); + + _GeofenceChannel(this._channel); + + void addGeofenceRegion(GeofenceRegion region) { + _invokeChannelMethod(_loggingTag, _channel, 'addGeofenceRegion', + _Codec.encodeGeofenceRegion(region)); + } + + void removeGeofenceRegion(GeofenceRegion region) { + _invokeChannelMethod(_loggingTag, _channel, 'removeGeofenceRegion', + _Codec.encodeGeofenceRegion(region)); + } + + Future> geofenceRegions() async { + final data = await _invokeChannelMethod(_loggingTag, _channel, 'geofenceRegions'); + return _Codec.decodeGeofenceRegions(data); + } +} diff --git a/lib/channel/location_channel.dart b/lib/channel/location_channel.dart index b8cbfcd..48827c2 100644 --- a/lib/channel/location_channel.dart +++ b/lib/channel/location_channel.dart @@ -16,10 +16,9 @@ part of geolocation; // // * Subscriptions share a single location request on the platform but each subscriptions must be independent. // A single location subscription should be closed after a single result was listened to while a continuous update subscription will continue -// until owner cancels it. +// until owner cancels it.e class _LocationChannel { - static const MethodChannel _channel = - const MethodChannel('geolocation/location'); + final MethodChannel _channel; static const EventChannel _updatesChannel = const EventChannel('geolocation/locationUpdates'); @@ -34,6 +33,8 @@ class _LocationChannel { final List<_LocationUpdatesSubscription> _updatesSubscriptions = []; + _LocationChannel(this._channel); + Future isLocationOperational( LocationPermission permission) async { final response = await _invokeChannelMethod(_loggingTag, _channel, diff --git a/lib/data/location.dart b/lib/data/location.dart index 9c15182..2fd9521 100644 --- a/lib/data/location.dart +++ b/lib/data/location.dart @@ -4,7 +4,8 @@ part of geolocation; class Location { - Location._(this.latitude, this.longitude, this.altitude); + Location( + {@required this.latitude, @required this.longitude, this.altitude = 0.0}); /// Latitude in degrees final double latitude; diff --git a/lib/data/region.dart b/lib/data/region.dart new file mode 100644 index 0000000..8022d3f --- /dev/null +++ b/lib/data/region.dart @@ -0,0 +1,59 @@ +// Licensed under Apache License v2.0 + +part of geolocation; + +/// Represents a circular region +class Region { + Region({@required this.center, @required this.radius}); + + /// Center of the circular region + final Location center; + + /// Radius measured in meters. + final double radius; + + @override + String toString() { + return '{center: $center, radius: $radius}'; + } +} + +class GeofenceRegion { + final Region region; + final String id; + final bool notifyOnEntry; + final bool notifyOnExit; + + GeofenceRegion( + {@required this.region, + @required this.id, + this.notifyOnEntry = true, + this.notifyOnExit = false}); + + @override + String toString() { + return '{region: $region, id: $id, notifyOnEntry: $notifyOnEntry, notifyOnExit: $notifyOnExit}'; + } +} + +enum GeofenceEventType { entered, exited } + +class GeofenceEvent { + final GeofenceEventType type; + final GeofenceRegion geofenceRegion; + + GeofenceEvent._(this.type, this.geofenceRegion); +} + +class GeofenceEventResult extends GeolocationResult { + GeofenceEventResult._( + bool isSuccessful, GeolocationResultError error, this.geofenceEvent) + : super._(isSuccessful, error); + + final GeofenceEvent geofenceEvent; + + @override + String dataToString() { + return geofenceEvent.toString(); + } +} diff --git a/lib/geolocation.dart b/lib/geolocation.dart index fed0edb..9cb1bdc 100644 --- a/lib/geolocation.dart +++ b/lib/geolocation.dart @@ -13,6 +13,8 @@ import 'package:flutter/services.dart'; part 'channel/codec.dart'; +part 'channel/geofence_channel.dart'; + part 'channel/helper.dart'; part 'channel/location_channel.dart'; @@ -25,6 +27,8 @@ part 'data/location_result.dart'; part 'data/permission.dart'; +part 'data/region.dart'; + part 'data/result.dart'; part 'facet_android/location.dart'; @@ -212,10 +216,33 @@ class Geolocation { displacementFilter, )); + static Stream get geofenceUpdates { + return _geofenceChannel._geofenceUpdates; + } + + static void addGeofenceRegion(GeofenceRegion region) { + return _geofenceChannel.addGeofenceRegion(region); + } + + static void removeGeofenceRegion(GeofenceRegion region) { + return _geofenceChannel.removeGeofenceRegion(region); + } + + static Future> geofenceRegions() { + return _geofenceChannel.geofenceRegions(); + } + /// Activate verbose logging for debugging purposes. static bool loggingEnabled = false; - static final _LocationChannel _locationChannel = new _LocationChannel(); + static const MethodChannel _channel = + const MethodChannel('geolocation/location'); + + static final _LocationChannel _locationChannel = + new _LocationChannel(_channel); + + static final _GeofenceChannel _geofenceChannel = + new _GeofenceChannel(_channel); } class GeolocationException implements Exception { From 4cad6e263168b08c0ab17bc785dbf1799216e500 Mon Sep 17 00:00:00 2001 From: Manuel Adameit Date: Fri, 6 Jul 2018 21:57:48 +0200 Subject: [PATCH 2/6] Show active geofence regions --- example/lib/tab_geofence.dart | 46 ++++++++++++++++++++--- ios/Classes/Location/LocationClient.swift | 2 - 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/example/lib/tab_geofence.dart b/example/lib/tab_geofence.dart index 2a111b1..430eb38 100644 --- a/example/lib/tab_geofence.dart +++ b/example/lib/tab_geofence.dart @@ -18,6 +18,13 @@ class _TabGeofenceState extends State { List _geofenceEventResults = []; StreamSubscription _subscription; bool _isTracking = false; + List _geofenceRegions = []; + + @override + void initState() { + super.initState(); + _updateRegionsList(); + } @override dispose() { @@ -60,7 +67,7 @@ class _TabGeofenceState extends State { region: Region( center: Location(longitude: 10.0, latitude: 10.0), radius: 10.0)); Geolocation.addGeofenceRegion(region); - print(await Geolocation.geofenceRegions()); + _updateRegionsList(); } _onRemovePressed() async { @@ -71,7 +78,15 @@ class _TabGeofenceState extends State { region: Region( center: Location(longitude: 10.0, latitude: 10.0), radius: 10.0)); Geolocation.removeGeofenceRegion(region); - print(await Geolocation.geofenceRegions()); + final regions = await Geolocation.geofenceRegions(); + print(regions); + _updateRegionsList(); + } + + _updateRegionsList() async { + _geofenceRegions = await Geolocation.geofenceRegions(); + setState(() { + }); } @override @@ -84,12 +99,33 @@ class _TabGeofenceState extends State { onRemovePressed: _onRemovePressed, ) ]; + + if (_geofenceRegions.length != 0) { + children.add(ListTile( + title: Text( + "Active Geofence regions:", + style: Theme.of(context).textTheme.subhead, + ), + )); + + children.addAll(ListTile.divideTiles( + context: context, + tiles: _geofenceRegions.map((geofenceRegion) { + return ListTile( + title: new Text("${geofenceRegion.id}"), + subtitle: Text("Latitude: ${geofenceRegion.region.center + .latitude}\nLongitude: ${geofenceRegion.region.center + .longitude}"), + ); + }).toList(), + )); + children.add(Divider(height: 32.0,)); + } children.addAll(ListTile.divideTiles( context: context, - tiles: _geofenceEventResults - .map((location) => new _Item(data: location)) - .toList(), + tiles: + _geofenceEventResults.map((event) => new _Item(data: event)).toList(), )); return new Scaffold( diff --git a/ios/Classes/Location/LocationClient.swift b/ios/Classes/Location/LocationClient.swift index e04f968..e8424d9 100644 --- a/ios/Classes/Location/LocationClient.swift +++ b/ios/Classes/Location/LocationClient.swift @@ -244,7 +244,6 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { - NSLog("didEnterRegion") guard let region = region as? CLCircularRegion else { NSLog("Expected circular region, ignoring event.") return @@ -255,7 +254,6 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { - NSLog("didExitRegion") guard let region = region as? CLCircularRegion else { NSLog("Expected circular region, ignoring event.") return From 9d298bb9e5fc05446171af3f7ff40b32bc9b05ab Mon Sep 17 00:00:00 2001 From: Manuel Adameit Date: Sun, 8 Jul 2018 15:01:35 +0200 Subject: [PATCH 3/6] Queue unobserverd geofence events Safe and restore last observing state in example Send notifications on events --- example/lib/tab_geofence.dart | 46 +++++++++++++++-------- example/pubspec.yaml | 2 + ios/Classes/Location/LocationClient.swift | 22 +++++++++-- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/example/lib/tab_geofence.dart b/example/lib/tab_geofence.dart index 430eb38..762bba1 100644 --- a/example/lib/tab_geofence.dart +++ b/example/lib/tab_geofence.dart @@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:geolocation/geolocation.dart'; +import 'package:local_notifications/local_notifications.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class TabGeofence extends StatefulWidget { @override @@ -19,11 +21,18 @@ class _TabGeofenceState extends State { StreamSubscription _subscription; bool _isTracking = false; List _geofenceRegions = []; + SharedPreferences sharedPreferences; @override - void initState() { + initState() { super.initState(); _updateRegionsList(); + _initTracking(); + } + + _initTracking() async { + sharedPreferences = await SharedPreferences.getInstance(); + _setTracking(sharedPreferences.getBool('isTracking') ?? _isTracking); } @override @@ -32,26 +41,28 @@ class _TabGeofenceState extends State { _subscription.cancel(); } - _onTogglePressed() { - if (_isTracking) { - setState(() { - _isTracking = false; - }); + _setTracking(shouldTrack) { + sharedPreferences.setBool('isTracking', shouldTrack); + setState(() { + _isTracking = shouldTrack; + }); - _subscription.cancel(); + if (!_isTracking) { + _subscription?.cancel(); _subscription = null; } else { - setState(() { - _isTracking = true; - }); - _subscription = Geolocation.geofenceUpdates.listen((result) { + LocalNotifications.createNotification( + title: result.geofenceEvent.geofenceRegion.id, + content: result.geofenceEvent.type.toString() + ' happened!', + id: 0); setState(() { _geofenceEventResults.insert(0, result); }); }); _subscription.onDone(() { + print('The Geolocation plugin requested to stop the geofence tracking'); setState(() { _isTracking = false; }); @@ -59,6 +70,10 @@ class _TabGeofenceState extends State { } } + _onTogglePressed() { + _setTracking(!_isTracking); + } + _onAddPressed() async { final region = GeofenceRegion( id: "MyHome", @@ -85,8 +100,7 @@ class _TabGeofenceState extends State { _updateRegionsList() async { _geofenceRegions = await Geolocation.geofenceRegions(); - setState(() { - }); + setState(() {}); } @override @@ -99,7 +113,7 @@ class _TabGeofenceState extends State { onRemovePressed: _onRemovePressed, ) ]; - + if (_geofenceRegions.length != 0) { children.add(ListTile( title: Text( @@ -119,7 +133,9 @@ class _TabGeofenceState extends State { ); }).toList(), )); - children.add(Divider(height: 32.0,)); + children.add(Divider( + height: 32.0, + )); } children.addAll(ListTile.divideTiles( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cd8b70f..a4e73af 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,6 +2,8 @@ name: geolocation_example description: Demonstrates how to use the geolocation plugin. dependencies: + shared_preferences: + local_notifications: flutter: sdk: flutter diff --git a/ios/Classes/Location/LocationClient.swift b/ios/Classes/Location/LocationClient.swift index e8424d9..5089fa1 100644 --- a/ios/Classes/Location/LocationClient.swift +++ b/ios/Classes/Location/LocationClient.swift @@ -22,6 +22,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } private var geofenceUpdatesCallback: GeofenceUpdatesCallback? = nil + private var unobservedGeofenceEventQueue: [Result] = [] private var isPaused = false @@ -78,6 +79,13 @@ class LocationClient : NSObject, CLLocationManagerDelegate { func registerGeofenceUpdates(callback: @escaping GeofenceUpdatesCallback) { precondition(geofenceUpdatesCallback == nil, "trying to register a 2nd location geofence callback") geofenceUpdatesCallback = callback + + // send queued elements + for geofenceEvent in unobservedGeofenceEventQueue { + callback(geofenceEvent) + } + // reset list + unobservedGeofenceEventQueue = [] } func deregisterGeofenceUpdatesCallback() { @@ -250,7 +258,7 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } let geofenceEvent = GeofenceEvent(region: region, type: .entered) - geofenceUpdatesCallback?(Result.success(with: geofenceEvent)) + handleGeofenceEvent(geofenceEvent) } public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) { @@ -260,9 +268,17 @@ class LocationClient : NSObject, CLLocationManagerDelegate { } let geofenceEvent = GeofenceEvent(region: region, type: .exited) - geofenceUpdatesCallback?(Result.success(with: geofenceEvent)) + handleGeofenceEvent(geofenceEvent) } - + + private func handleGeofenceEvent(_ geofenceEvent: GeofenceEvent) { + if let callback = geofenceUpdatesCallback { + callback(Result.success(with: geofenceEvent)) + } else { + unobservedGeofenceEventQueue.append(Result.success(with: geofenceEvent)) + } + } + struct Callback { let success: (T) -> Void let failure: (E) -> Void From c37d11bad1b2541c6511e53fd80c64db72f03f78 Mon Sep 17 00:00:00 2001 From: GDur Date: Wed, 11 Jul 2018 23:24:23 +0200 Subject: [PATCH 4/6] Started the geofence projekt for android --- .../geolocation/GeolocationPlugin.kt | 2 +- .../intheloup/geolocation/LocationChannel.kt | 57 +++++++-- .../geolocation/location/LocationClient.kt | 109 +++++++++++++++++- .../android/app/src/main/AndroidManifest.xml | 7 +- example/android/build.gradle | 2 +- 5 files changed, 163 insertions(+), 14 deletions(-) diff --git a/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt b/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt index a6d78e1..5d1d535 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt +++ b/android/src/main/kotlin/io/intheloup/geolocation/GeolocationPlugin.kt @@ -11,7 +11,7 @@ import io.intheloup.geolocation.location.LocationClient class GeolocationPlugin(val registrar: Registrar) { - private val locationClient = LocationClient(registrar.activity()) + private val locationClient = LocationClient(registrar.activity(), registrar.context()) private val locationChannel = LocationChannel(locationClient) init { diff --git a/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt b/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt index 64715e9..c03e0c3 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt +++ b/android/src/main/kotlin/io/intheloup/geolocation/LocationChannel.kt @@ -12,14 +12,20 @@ import io.intheloup.geolocation.location.LocationClient import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.launch -class LocationChannel(private val locationClient: LocationClient) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { +class LocationChannel(private val locationClient: LocationClient) : MethodChannel.MethodCallHandler { fun register(plugin: GeolocationPlugin) { + val locationUpdatesHandler = LocationUpdatesHandler(locationClient) + val geofenceUpdatesHandler = GeofenceUpdatesHandler(locationClient) + val methodChannel = MethodChannel(plugin.registrar.messenger(), "geolocation/location") methodChannel.setMethodCallHandler(this) - val eventChannel = EventChannel(plugin.registrar.messenger(), "geolocation/locationUpdates") - eventChannel.setStreamHandler(this) + val locationEventChannel = EventChannel(plugin.registrar.messenger(), "geolocation/locationUpdates") + locationEventChannel.setStreamHandler(locationUpdatesHandler) + + val geofenceEventChannel = EventChannel(plugin.registrar.messenger(), "geolocation/geofenceUpdates") + geofenceEventChannel.setStreamHandler(geofenceUpdatesHandler) } private fun isLocationOperational(permission: Permission, result: MethodChannel.Result) { @@ -48,7 +54,20 @@ class LocationChannel(private val locationClient: LocationClient) : MethodChanne locationClient.removeLocationUpdatesRequest(id) } + // geofencing + private fun addGeofenceRegion() { + locationClient.addGeofenceRegion() + } + + private fun removeGeofenceRegion() { + } + + private fun geofenceRegions(result: MethodChannel.Result) { + launch(UI) { + result.success("[]") + } + } // MethodChannel.MethodCallHandler override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { @@ -58,20 +77,40 @@ class LocationChannel(private val locationClient: LocationClient) : MethodChanne "lastKnownLocation" -> lastKnownLocation(Codec.decodePermission(call.arguments), result) "addLocationUpdatesRequest" -> addLocationUpdatesRequest(Codec.decodeLocationUpdatesRequest(call.arguments)) "removeLocationUpdatesRequest" -> removeLocationUpdatesRequest(Codec.decodeInt(call.arguments)) +// "addGeofenceRegion" -> addGeofenceRegion(Codec.decodeGeofenceRegion(call.arguments)) + "addGeofenceRegion" -> addGeofenceRegion() + "removeGeofenceRegion" -> removeGeofenceRegion() + "geofenceRegions" -> geofenceRegions(result) else -> result.notImplemented() } } + class LocationUpdatesHandler(private val locationClient: LocationClient) : EventChannel.StreamHandler { - // EventChannel.StreamHandler + // EventChannel.StreamHandler + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + locationClient.registerLocationUpdatesCallback { result -> + events.success(Codec.encodeResult(result)) + } + } - override fun onListen(arguments: Any?, events: EventChannel.EventSink) { - locationClient.registerLocationUpdatesCallback { result -> - events.success(Codec.encodeResult(result)) + override fun onCancel(arguments: Any?) { + locationClient.deregisterLocationUpdatesCallback() } } - override fun onCancel(arguments: Any?) { - locationClient.deregisterLocationUpdatesCallback() + class GeofenceUpdatesHandler(private val locationClient: LocationClient) : EventChannel.StreamHandler { + // EventChannel.StreamHandler + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { +// locationClient.registerGeofenceUpdatesCallback { result -> +// events.success(Codec.encodeResult(result)) +// } + } + + override fun onCancel(arguments: Any?) { +// locationClient.deregisterGeofenceUpdatesCallback() + } } + + } \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt index de241f9..70c2249 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt +++ b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt @@ -5,9 +5,14 @@ package io.intheloup.geolocation.location import android.annotation.SuppressLint import android.app.Activity +import android.app.IntentService +import android.app.PendingIntent +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.location.Location import android.support.v4.app.ActivityCompat +import android.util.Log import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.location.* @@ -22,7 +27,10 @@ import kotlinx.coroutines.experimental.launch import kotlin.coroutines.experimental.suspendCoroutine @SuppressLint("MissingPermission") -class LocationClient(private val activity: Activity) { +class LocationClient( + private val activity: Activity, + private val context: Context +) { val permissionResultListener: PluginRegistry.RequestPermissionsResultListener = PluginRegistry.RequestPermissionsResultListener { id, _, grantResults -> if (id == GeolocationPlugin.Intents.LocationPermissionRequestId) { @@ -99,7 +107,6 @@ class LocationClient(private val activity: Activity) { // Updates API - suspend fun addLocationUpdatesRequest(request: LocationUpdatesRequest) { val validity = validateServiceStatus(request.permission) if (!validity.isValid) { @@ -126,6 +133,104 @@ class LocationClient(private val activity: Activity) { locationUpdatesCallback = null } + private val geofencePendingIntent: PendingIntent by lazy { + val intent = Intent(context, GeofenceTransitionsIntentService::class.java) + // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when calling + // addGeofences() and removeGeofences(). + PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + + class GeofenceTransitionsIntentService : IntentService("GeofenceTransitionsIntentService Thread") { + + override fun onHandleIntent(intent: Intent?) { + + val geofencingEvent = GeofencingEvent.fromIntent(intent) + if (geofencingEvent.hasError()) { +// val errorMessage = GeofenceErrorMessages.getErrorString(this, +// geofencingEvent.errorCode) +// Log.e(TAG, errorMessage) + return + } + + // Get the transition type. + val geofenceTransition = geofencingEvent.geofenceTransition + + // Test that the reported transition was of interest. + if ( + geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || + geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT + ) { + + // Get the geofences that were triggered. A single event can trigger + // multiple geofences. + val triggeringGeofences = geofencingEvent.triggeringGeofences + + // Get the transition details as a String. +// val geofenceTransitionDetails = getGeofenceTransitionDetails( +// this, +// geofenceTransition, +// triggeringGeofences +// ) + + // Send notification and log the transition details. +// sendNotification(geofenceTransitionDetails) + Log.i("BLA", geofenceTransition.toString()) + } else { + // Log the error. +// Log.e(TAG, getString(R.string.geofence_transition_invalid_type, +// geofenceTransition)) + } + } + } + + /** + * geofencing + */ + fun addGeofenceRegion() { + val region = Geofence.Builder() + // Set the request ID of the geofence. This is a string to identify this + // geofence. + .setRequestId("MyHome") + + // Set the circular region of this geofence. + .setCircularRegion( + 10.0, + 10.0, + 23.0f + ) + + // Set the expiration duration of the geofence. This geofence gets automatically + // removed after this period of time. + .setExpirationDuration(60 * 60 * 1000 * 99) + + // Set the transition types of interest. Alerts are only generated for these + // transition. We track entry and exit transitions in this sample. + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT) + + // Create the geofence. + .build() + + val regions: List = mutableListOf(region) + + val request = GeofencingRequest.Builder().apply { + setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) + addGeofences(regions) + }.build() + +// val geo: GeofenceTransitionsIntentService = GeofenceTransitionsIntentService() + + LocationServices.getGeofencingClient(activity)?.addGeofences(request, geofencePendingIntent)?.run { + addOnSuccessListener { + // Geofences added + Log.i("GEOFENCE", "SUCCESESS") + } + addOnFailureListener { + Log.i("GEOFENCE", "FAIL") + // Failed to add geofences + } + } + + } // Lifecycle API diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 5c79788..1de168d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -16,10 +16,15 @@ FlutterApplication and put your custom class here. --> + + Date: Fri, 13 Jul 2018 11:33:29 +0200 Subject: [PATCH 5/6] GeofenceTransitionsIntentService should work but does not --- .../GeofenceTransitionsIntentService.kt | 50 +++++++++ .../geolocation/data/GeofenceData.kt | 79 ++++++++++++++ .../geolocation/location/LocationClient.kt | 101 ++++++++++-------- .../android/app/src/main/AndroidManifest.xml | 2 +- 4 files changed, 184 insertions(+), 48 deletions(-) create mode 100644 android/src/main/kotlin/io/intheloup/geolocation/GeofenceTransitionsIntentService.kt create mode 100644 android/src/main/kotlin/io/intheloup/geolocation/data/GeofenceData.kt diff --git a/android/src/main/kotlin/io/intheloup/geolocation/GeofenceTransitionsIntentService.kt b/android/src/main/kotlin/io/intheloup/geolocation/GeofenceTransitionsIntentService.kt new file mode 100644 index 0000000..500f82c --- /dev/null +++ b/android/src/main/kotlin/io/intheloup/geolocation/GeofenceTransitionsIntentService.kt @@ -0,0 +1,50 @@ +package io.intheloup.geolocation + +import android.app.IntentService +import android.content.Intent +import android.util.Log +import com.google.android.gms.location.Geofence +import com.google.android.gms.location.GeofencingEvent + +class GeofenceTransitionsIntentService : IntentService("GeofenceTransitionsIntentService") { + + override fun onHandleIntent(intent: Intent?) { + + val geofencingEvent = GeofencingEvent.fromIntent(intent) + if (geofencingEvent.hasError()) { +// val errorMessage = GeofenceErrorMessages.getErrorString(this, +// geofencingEvent.errorCode) +// Log.e(TAG, errorMessage) + return + } + + // Get the transition type. + val geofenceTransition = geofencingEvent.geofenceTransition + + // Test that the reported transition was of interest. + if ( + geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || + geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT + ) { + + // Get the geofences that were triggered. A single event can trigger + // multiple geofences. + val triggeringGeofences = geofencingEvent.triggeringGeofences + + // Get the transition details as a String. +// val geofenceTransitionDetails = getGeofenceTransitionDetails( +// this, +// geofenceTransition, +// triggeringGeofences +// ) + + // Send notification and log the transition details. +// sendNotification(geofenceTransitionDetails) + Log.i("BLA", geofenceTransition.toString()) + } else { + // Log the error. +// Log.e(TAG, getString(R.string.geofence_transition_invalid_type, +// geofenceTransition)) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/io/intheloup/geolocation/data/GeofenceData.kt b/android/src/main/kotlin/io/intheloup/geolocation/data/GeofenceData.kt new file mode 100644 index 0000000..025192e --- /dev/null +++ b/android/src/main/kotlin/io/intheloup/geolocation/data/GeofenceData.kt @@ -0,0 +1,79 @@ +// Copyright (c) 2018 Loup Inc. +// Licensed under Apache License v2.0 + +package io.intheloup.geolocation.data + +import com.google.android.gms.location.Geofence + +class GeofenceData(val id: String, +// val region: Region, + val notifyOnEntry: Boolean, + val notifyOnExit: Boolean +) { + companion object { + fun from(geofence: Geofence) = GeofenceData( + id = geofence.requestId, + + notifyOnEntry = false, + notifyOnExit = false + ) + } +} + + +//class Region( +// val center: Location, +// val radius: Double) { +// companion object { +// fun from(location: Location) = LocationData( +// latitude = location.latitude, +// longitude = location.longitude, +// altitude = location.altitude +// ) +// } +// init(from region: CLCircularRegion) +// { +// self.center = Location(from: region. center) +// self.radius = region.radius +// } +//} + +//class GeofenceRegion( +// val region: Region, +// val id: String, +// val notifyOnEntry: Bool, +// val notifyOnExit: Bool){ +// { +// companion object { +// fun from(geofence: Geofence) = Geofence( +// latitude = location.latitude, +// longitude = location.longitude, +// altitude = location.altitude +// self.region = Region(from: region) +// self.id = region.identifier +// self.notifyOnEntry = region.notifyOnEntry +// self.notifyOnExit = region.notifyOnExit +// ) +// } +// +// var clRegion: CLCircularRegion { +// val result = CLCircularRegion(center: self. region . center . coordinate2D, radius: self.region.radius, identifier: self.id) +// result.notifyOnExit = self.notifyOnExit +// result.notifyOnEntry = self.notifyOnEntry +// return result +// } +//} +// +//enum GeofenceEventType: String, Codable { +// case entered, exited +//} +// +//data class GeofenceEvent ( +// val type: GeofenceEventType +// val geofenceRegion: GeofenceRegion +//){ +// init(region: CLCircularRegion, type: GeofenceEventType) { +// self.type = type +// self.geofenceRegion = GeofenceRegion(from: region) +// } +//} diff --git a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt index 70c2249..d64a68b 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt +++ b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt @@ -18,13 +18,13 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.location.* import io.flutter.plugin.common.PluginRegistry import io.intheloup.geolocation.GeolocationPlugin -import io.intheloup.geolocation.data.LocationData -import io.intheloup.geolocation.data.LocationUpdatesRequest -import io.intheloup.geolocation.data.Permission -import io.intheloup.geolocation.data.Result import kotlinx.coroutines.experimental.android.UI import kotlinx.coroutines.experimental.launch import kotlin.coroutines.experimental.suspendCoroutine +import com.google.android.gms.location.Geofence +import io.intheloup.geolocation.GeofenceTransitionsIntentService +import io.intheloup.geolocation.data.* + @SuppressLint("MissingPermission") class LocationClient( @@ -86,6 +86,54 @@ class LocationClient( } } + + private suspend fun geofenceRegions(): Geofence? = suspendCoroutine { cont -> + cont.resume(Geofence.Builder() + // Set the request ID of the geofence. This is a string to identify this + // geofence. + .setRequestId("just an example") + + // Set the circular region of this geofence. + .setCircularRegion( + 10.0, + 10.0, + 10.0f + ) + + // Set the expiration duration of the geofence. This geofence gets automatically + // removed after this period of time. + .setExpirationDuration(1000*60*60*99) + + // Set the transition types of interest. Alerts are only generated for these + // transition. We track entry and exit transitions in this sample. + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT) + + // Create the geofence. + .build()) +// providerClient.lastLocation +// .addOnSuccessListener { location: Location? -> cont.resume(location) } +// .addOnFailureListener { cont.resumeWithException(it) } + } + + suspend fun geofenceRegions(permission: Permission): Result { + val validity = validateServiceStatus(permission) + if (!validity.isValid) { + return validity.failure!! + } + + val geofence = try { + geofenceRegions() + } catch (e: Exception) { + return Result.failure(Result.Error.Type.Runtime, message = e.message) + } + + return if (geofence != null) { + Result.success(arrayOf(GeofenceData.from(geofence))) + } else { + Result.failure(Result.Error.Type.LocationNotFound) + } + } + suspend fun lastKnownLocation(permission: Permission): Result { val validity = validateServiceStatus(permission) if (!validity.isValid) { @@ -140,48 +188,7 @@ class LocationClient( PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) } - class GeofenceTransitionsIntentService : IntentService("GeofenceTransitionsIntentService Thread") { - - override fun onHandleIntent(intent: Intent?) { - - val geofencingEvent = GeofencingEvent.fromIntent(intent) - if (geofencingEvent.hasError()) { -// val errorMessage = GeofenceErrorMessages.getErrorString(this, -// geofencingEvent.errorCode) -// Log.e(TAG, errorMessage) - return - } - // Get the transition type. - val geofenceTransition = geofencingEvent.geofenceTransition - - // Test that the reported transition was of interest. - if ( - geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER || - geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT - ) { - - // Get the geofences that were triggered. A single event can trigger - // multiple geofences. - val triggeringGeofences = geofencingEvent.triggeringGeofences - - // Get the transition details as a String. -// val geofenceTransitionDetails = getGeofenceTransitionDetails( -// this, -// geofenceTransition, -// triggeringGeofences -// ) - - // Send notification and log the transition details. -// sendNotification(geofenceTransitionDetails) - Log.i("BLA", geofenceTransition.toString()) - } else { - // Log the error. -// Log.e(TAG, getString(R.string.geofence_transition_invalid_type, -// geofenceTransition)) - } - } - } /** * geofencing @@ -201,7 +208,7 @@ class LocationClient( // Set the expiration duration of the geofence. This geofence gets automatically // removed after this period of time. - .setExpirationDuration(60 * 60 * 1000 * 99) + .setExpirationDuration(-1) // Set the transition types of interest. Alerts are only generated for these // transition. We track entry and exit transitions in this sample. @@ -213,7 +220,7 @@ class LocationClient( val regions: List = mutableListOf(region) val request = GeofencingRequest.Builder().apply { - setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) + setInitialTrigger(Geofence .GEOFENCE_TRANSITION_EXIT) addGeofences(regions) }.build() diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 1de168d..107d27d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -22,7 +22,7 @@ tools:ignore="GoogleAppIndexingWarning"> Date: Mon, 17 Sep 2018 12:50:10 +0200 Subject: [PATCH 6/6] Changed setInitialTrigger(Geofence .GEOFENCE_TRANSITION_EXIT) to setInitialTrigger(Geofence.GEOFENCE_TRANSITION_ENTER) --- .../kotlin/io/intheloup/geolocation/location/LocationClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt index d64a68b..5dde78d 100644 --- a/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt +++ b/android/src/main/kotlin/io/intheloup/geolocation/location/LocationClient.kt @@ -220,7 +220,7 @@ class LocationClient( val regions: List = mutableListOf(region) val request = GeofencingRequest.Builder().apply { - setInitialTrigger(Geofence .GEOFENCE_TRANSITION_EXIT) + setInitialTrigger(Geofence.GEOFENCE_TRANSITION_ENTER) addGeofences(regions) }.build()