diff --git a/packages/carp_connectivity_package/lib/connectivity.dart b/packages/carp_connectivity_package/lib/connectivity.dart index 87460d82..4095965b 100644 --- a/packages/carp_connectivity_package/lib/connectivity.dart +++ b/packages/carp_connectivity_package/lib/connectivity.dart @@ -7,6 +7,7 @@ library; import 'dart:async'; import 'dart:convert'; +import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:connectivity_plus/connectivity_plus.dart' as connectivity; diff --git a/packages/carp_connectivity_package/lib/connectivity_data.dart b/packages/carp_connectivity_package/lib/connectivity_data.dart index c36bf80d..272a614b 100644 --- a/packages/carp_connectivity_package/lib/connectivity_data.dart +++ b/packages/carp_connectivity_package/lib/connectivity_data.dart @@ -41,23 +41,17 @@ class Connectivity extends Data { Connectivity() : super(); - Connectivity.fromConnectivityResult( - List result) - : super() { - connectivityStatus = result - .map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)) - .toList(); + Connectivity.fromConnectivityResult(List result) : super() { + connectivityStatus = result.map((connectivity.ConnectivityResult e) => _parseConnectivityStatus(e)).toList(); } @override Function get fromJsonFunction => _$ConnectivityFromJson; - factory Connectivity.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Connectivity.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$ConnectivityToJson(this); - static ConnectivityStatus _parseConnectivityStatus( - connectivity.ConnectivityResult result) { + static ConnectivityStatus _parseConnectivityStatus(connectivity.ConnectivityResult result) { switch (result) { case connectivity.ConnectivityResult.bluetooth: return ConnectivityStatus.bluetooth; @@ -77,8 +71,7 @@ class Connectivity extends Data { } @override - String toString() => - '${super.toString()}, connectivityStatus: $connectivityStatus'; + String toString() => '${super.toString()}, connectivityStatus: $connectivityStatus'; } /// A [Data] holding information of nearby Bluetooth devices. @@ -98,15 +91,14 @@ class Bluetooth extends Data { /// The list of [BluetoothDevice] found in a scan. List get scanResult => _scanResult.values.toList(); - set scanResult(List devices) => _scanResult.addEntries( - devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); + set scanResult(List devices) => + _scanResult.addEntries(devices.map((device) => MapEntry(device.bluetoothDeviceId, device))); Bluetooth({DateTime? startScan, this.endScan}) : super() { this.startScan = startScan ?? DateTime.now(); } - void addBluetoothDevice(BluetoothDevice device) => - _scanResult[device.bluetoothDeviceId] = device; + void addBluetoothDevice(BluetoothDevice device) => _scanResult[device.bluetoothDeviceId] = device; void addBluetoothDevicesFromScanResults(List results) { for (var scanResult in results) { @@ -114,10 +106,16 @@ class Bluetooth extends Data { } } + void addBluetoothDevicesFromRangingResults( + Beacon result, + String beaconName, + ) { + addBluetoothDevice(BluetoothDevice.fromRangingResult(result, beaconName)); + } + @override Function get fromJsonFunction => _$BluetoothFromJson; - factory Bluetooth.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Bluetooth.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$BluetoothToJson(this); @@ -164,8 +162,16 @@ class BluetoothDevice { rssi: result.rssi, ); - factory BluetoothDevice.fromJson(Map json) => - _$BluetoothDeviceFromJson(json); + factory BluetoothDevice.fromRangingResult(Beacon result, String beaconName) => BluetoothDevice( + bluetoothDeviceId: beaconName, + bluetoothDeviceName: beaconName, + connectable: false, + txPowerLevel: result.txPower, + advertisementName: beaconName, + rssi: result.rssi, + ); + + factory BluetoothDevice.fromJson(Map json) => _$BluetoothDeviceFromJson(json); Map toJson() => _$BluetoothDeviceToJson(this); @override @@ -202,12 +208,49 @@ class Wifi extends Data { @override Function get fromJsonFunction => _$WifiFromJson; - factory Wifi.fromJson(Map json) => - FromJsonFactory().fromJson(json); + factory Wifi.fromJson(Map json) => FromJsonFactory().fromJson(json); @override Map toJson() => _$WifiToJson(this); @override - String toString() => - '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; + String toString() => '${super.toString()}, SSID: $ssid, BSSID: $bssid, IP: $ip'; +} + +/// Beacon Region to use when monitoring for beacons. +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class BeaconRegion { + /// A unique identifier for the beacon region. + /// Used to distinguish between different regions being monitored. + String identifier; + + /// The proximity UUID of the beacon. + /// This is a 128-bit value used to identify a group of related beacons. + String uuid; + + /// The major value of the beacon region (optional). + /// Used to further distinguish a subset of beacons within the same UUID. + int? major; + + /// The minor value of the beacon region (optional). + /// Provides a finer granularity within a group of beacons identified by the same UUID and major value. + int? minor; + + BeaconRegion({ + required this.identifier, + required this.uuid, + this.major, + this.minor, + }); + + Region toRegion() { + return Region( + identifier: identifier, + proximityUUID: uuid, + major: major, + minor: minor, + ); + } + + @override + String toString() => '${super.toString()}, Identifier: $identifier, UUID: $uuid, Major: $major, Minor: $minor'; } diff --git a/packages/carp_connectivity_package/lib/connectivity_package.dart b/packages/carp_connectivity_package/lib/connectivity_package.dart index 6ec45b2b..df71576d 100644 --- a/packages/carp_connectivity_package/lib/connectivity_package.dart +++ b/packages/carp_connectivity_package/lib/connectivity_package.dart @@ -21,8 +21,7 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { static const String WIFI = "${NameSpace.CARP}.wifi"; @override - DataTypeSamplingSchemeMap get samplingSchemes => - DataTypeSamplingSchemeMap.from([ + DataTypeSamplingSchemeMap get samplingSchemes => DataTypeSamplingSchemeMap.from([ DataTypeSamplingScheme( CamsDataTypeMetaData( type: CONNECTIVITY, @@ -80,12 +79,8 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { ]); // registering default privacy functions - DataTransformerSchemaRegistry() - .lookup(PrivacySchema.DEFAULT)! - .add(BLUETOOTH, bluetoothNameAnonymizer); - DataTransformerSchemaRegistry() - .lookup(PrivacySchema.DEFAULT)! - .add(WIFI, wifiNameAnonymizer); + DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(BLUETOOTH, bluetoothNameAnonymizer); + DataTransformerSchemaRegistry().lookup(PrivacySchema.DEFAULT)!.add(WIFI, wifiNameAnonymizer); } } @@ -100,29 +95,39 @@ class ConnectivitySamplingPackage extends SmartphoneSamplingPackage { /// Filtering on remoteIds allows Android to scan for devices in the background /// without needing to be in the foreground. This is not possible on iOS. @JsonSerializable(includeIfNull: false, explicitToJson: true) -class BluetoothScanPeriodicSamplingConfiguration - extends PeriodicSamplingConfiguration { +class BluetoothScanPeriodicSamplingConfiguration extends PeriodicSamplingConfiguration { /// List of Bluetooth service UUIDs to filter the scan results. List withServices; /// List of remote device IDs to filter the scan results. List withRemoteIds; + /// Use Package `flutter_beacon` to enable beacon monitoring while the app is in background. + bool useBeaconMonitoring; + + /// List of beacon regions to monitor and/or range using the `flutter_beacon` package. + /// + /// When [useBeaconMonitoring] is true, the app will monitor these regions, potentially in the background if platform permissions and conditions allow. + List beaconRegions; + + /// When a device is within this distance from the beacon, a predefined event is triggered. + /// Defaults to 2 meters. + int beaconDistance; + BluetoothScanPeriodicSamplingConfiguration({ required super.interval, required super.duration, this.withServices = const [], this.withRemoteIds = const [], + this.beaconRegions = const [], + this.useBeaconMonitoring = false, + this.beaconDistance = 2, }); @override - Map toJson() => - _$BluetoothScanPeriodicSamplingConfigurationToJson(this); + Map toJson() => _$BluetoothScanPeriodicSamplingConfigurationToJson(this); @override - Function get fromJsonFunction => - _$BluetoothScanPeriodicSamplingConfigurationFromJson; - factory BluetoothScanPeriodicSamplingConfiguration.fromJson( - Map json) => - FromJsonFactory() - .fromJson(json); + Function get fromJsonFunction => _$BluetoothScanPeriodicSamplingConfigurationFromJson; + factory BluetoothScanPeriodicSamplingConfiguration.fromJson(Map json) => + FromJsonFactory().fromJson(json); } diff --git a/packages/carp_connectivity_package/lib/connectivity_probes.dart b/packages/carp_connectivity_package/lib/connectivity_probes.dart index 0649113b..6d774efe 100644 --- a/packages/carp_connectivity_package/lib/connectivity_probes.dart +++ b/packages/carp_connectivity_package/lib/connectivity_probes.dart @@ -13,18 +13,16 @@ class ConnectivityProbe extends StreamProbe { @override Future onStart() async { // collect the current connectivity status on sampling start - var connectivityStatus = - await connectivity.Connectivity().checkConnectivity(); - addMeasurement(Measurement.fromData( - Connectivity.fromConnectivityResult(connectivityStatus))); + var connectivityStatus = await connectivity.Connectivity().checkConnectivity(); + addMeasurement(Measurement.fromData(Connectivity.fromConnectivityResult(connectivityStatus))); return super.onStart(); } @override - Stream get stream => - connectivity.Connectivity().onConnectivityChanged.map((event) => - Measurement.fromData(Connectivity.fromConnectivityResult(event))); + Stream get stream => connectivity.Connectivity() + .onConnectivityChanged + .map((event) => Measurement.fromData(Connectivity.fromConnectivityResult(event))); } // This probe requests access to location permissions (both on Android and iOS). @@ -69,37 +67,50 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { Stream get bufferingStream => FlutterBluePlus.scanResults; @override - Future getMeasurement() async => - _data != null ? Measurement.fromData(_data!) : null; + Future getMeasurement() async => _data != null ? Measurement.fromData(_data!) : null; // if a BT-specific sampling configuration is used, we need to // extract the services and remoteIds from it so FlutterBluePlus can // perform filtered scanning - List get services => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withServices - .map((e) => Guid(e)) - .toList() + List get services => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withServices.map((e) => Guid(e)).toList() : []; - List get remoteIds => (samplingConfiguration - is BluetoothScanPeriodicSamplingConfiguration) - ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration) - .withRemoteIds + List get remoteIds => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).withRemoteIds : []; + bool get useBeaconMonitoring => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).useBeaconMonitoring + : false; + + List get beaconRegions => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconRegions + : []; + + int get beaconDistance => (samplingConfiguration is BluetoothScanPeriodicSamplingConfiguration) + ? (samplingConfiguration as BluetoothScanPeriodicSamplingConfiguration).beaconDistance + : 2; + + StreamSubscription? _streamMonitoring; + StreamSubscription? _streamRanging; + @override void onSamplingStart() { _data = Bluetooth(); try { - FlutterBluePlus.startScan( + if (useBeaconMonitoring) { + info('Using beacon monitoring.'); + _startMonitoring(); + } else { + FlutterBluePlus.startScan( withServices: services, withRemoteIds: remoteIds, - timeout: samplingConfiguration?.duration ?? - const Duration(milliseconds: DEFAULT_TIMEOUT)); + timeout: samplingConfiguration?.duration ?? const Duration(milliseconds: DEFAULT_TIMEOUT), + ); + } } catch (error) { FlutterBluePlus.stopScan(); _data = Error(message: 'Error scanning for bluetooth - $error'); @@ -108,7 +119,13 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { @override void onSamplingEnd() { - FlutterBluePlus.stopScan(); + if (useBeaconMonitoring) { + info('stopping monitoring'); + _stopMonitoring(); + } else { + FlutterBluePlus.stopScan(); + } + if (_data is Bluetooth) (_data as Bluetooth).endScan = DateTime.now(); } @@ -118,4 +135,52 @@ class BluetoothProbe extends BufferingPeriodicStreamProbe { (_data as Bluetooth).addBluetoothDevicesFromScanResults(event); } } + + Future _startMonitoring() async { + info('start monitoring & initializing scanning.'); + try { + await flutterBeacon.initializeScanning; + } catch (e) { + warning('error happened while initializing scanner $e'); + } + info('initialized scanner'); + + List regions = + beaconRegions.isEmpty ? [] : beaconRegions.map((beaconRegion) => beaconRegion!.toRegion()).toList(); + + try { + _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { + if (result.monitoringState == MonitoringState.inside) { + info('🚪 Entered region: ${result.region.identifier}'); + _startRanging(result.region); + } else if (result.monitoringState == MonitoringState.outside) { + info('Not in region: ${result.region.identifier}'); + _stopMonitoring(); + } + }); + } catch (e) { + info('Error starting monitoring: $e'); + } + } + + void _startRanging(Region region) { + _streamRanging = flutterBeacon.ranging([region]).listen((RangingResult result) { + final closeBeacons = result.beacons.where((beacon) => beacon.accuracy <= beaconDistance); + + for (var beacon in closeBeacons) { + info('✅ beacon in range: ${beacon.proximityUUID}, ${beacon.accuracy} m'); + (_data as Bluetooth).addBluetoothDevicesFromRangingResults( + beacon, + result.region.identifier, + ); + } + }); + } + + void _stopMonitoring() { + _streamRanging?.cancel(); + _streamRanging = null; + _streamMonitoring?.cancel(); + _streamMonitoring = null; + } } diff --git a/packages/carp_connectivity_package/pubspec.yaml b/packages/carp_connectivity_package/pubspec.yaml index 4e90cb06..fbd30542 100644 --- a/packages/carp_connectivity_package/pubspec.yaml +++ b/packages/carp_connectivity_package/pubspec.yaml @@ -21,7 +21,9 @@ dependencies: network_info_plus: ^6.0.0 # wifi ssid name flutter_blue_plus: ^1.35.0 # bluetooth scan crypto: ^3.0.0 # hashing sensitive data - permission_handler: '>=11.0.0 <13.0.0' + permission_handler: '>=11.0.0 <13.0.0' + dchs_flutter_beacon: ^0.6.4 + # Overriding carp libraries to use the local copy # Remove this before release of package diff --git a/packages/carp_esense_package/example/ios/Podfile b/packages/carp_esense_package/example/ios/Podfile new file mode 100644 index 00000000..d97f17e2 --- /dev/null +++ b/packages/carp_esense_package/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end