├── ios ├── Assets │ └── .gitkeep ├── .gitignore ├── Classes │ ├── FBBluetoothStateHandler.h │ ├── FBRangingStreamHandler.h │ ├── FBMonitoringStreamHandler.h │ ├── FBUtils.h │ ├── FBAuthorizationStatusHandler.h │ ├── FlutterBeaconPlugin.h │ ├── FBRangingStreamHandler.m │ ├── FBMonitoringStreamHandler.m │ ├── FBBluetoothStateHandler.m │ ├── FBAuthorizationStatusHandler.m │ └── FBUtils.m └── flutter_beacon.podspec ├── android ├── .idea │ ├── .name │ ├── caches │ │ ├── gradle_models.ser │ │ └── build_file_checksums.ser │ ├── vcs.xml │ ├── modules.xml │ ├── runConfigurations.xml │ ├── gradle.xml │ ├── misc.xml │ └── codeStyles │ │ └── Project.xml ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── flutterbeacon │ │ ├── FlutterBluetoothStateReceiver.java │ │ ├── FlutterBeaconBroadcast.java │ │ ├── FlutterPlatform.java │ │ ├── FlutterBeaconUtils.java │ │ └── FlutterBeaconScanner.java ├── build.gradle ├── gradlew.bat └── gradlew ├── example ├── test │ └── widget_test.dart ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── flutterbeaconexample │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── AppDelegate.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── main.m │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Podfile ├── .metadata ├── pubspec.yaml ├── lib │ ├── main.dart │ ├── controller │ │ └── requirement_state_controller.dart │ └── view │ │ ├── app_scanning.dart │ │ ├── app_broadcasting.dart │ │ └── home_page.dart ├── .gitignore └── README.md ├── AUTHORS ├── scripts └── credentials.sh ├── .metadata ├── .vscode └── launch.json ├── pubspec.yaml ├── .github └── workflows │ ├── flutter.yaml │ ├── pub-publish.yaml │ ├── pub-publish-test.yaml │ └── coverage-report.yaml ├── test └── beacon │ ├── beacon_broadcast_test.dart │ ├── ranging_result_test.dart │ ├── region_test.dart │ ├── authorization_status_test.dart │ ├── monitoring_result_test.dart │ ├── bluetooth_state_test.dart │ └── beacon_test.dart ├── Makefile ├── lib ├── beacon │ ├── ranging_result.dart │ ├── beacon_broadcast.dart │ ├── region.dart │ ├── monitoring_result.dart │ ├── authorization_status.dart │ ├── bluetooth_state.dart │ └── beacon.dart └── flutter_beacon.dart ├── CHANGELOG.md ├── flutter_beacon.iml ├── .idea └── codeStyles │ └── Project.xml ├── .gitignore ├── README.md └── LICENSE /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/.idea/.name: -------------------------------------------------------------------------------- 1 | flutter_beacon -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | void main() {} 2 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_beacon' 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/.idea/caches/gradle_models.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/android/.idea/caches/gradle_models.ser -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/android/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alann-maulana/flutter_beacon/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Alan Maulana -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/credentials.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -e ~/.pub-cache/credentials.json ]]; then 4 | mkdir -p ~/.pub-cache 5 | touch ~/.pub-cache/credentials.json 6 | fi 7 | 8 | echo $PUB_CREDENTIALS > ~/.pub-cache/credentials.json -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/flutterbeaconexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.flutterbeaconexample 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import com.flutterbeacon.FlutterBeaconPlugin; 5 | 6 | class MainActivity: FlutterActivity() { 7 | } 8 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 27 17:17:45 WIB 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_beacon_example 2 | description: Demonstrates how to use the flutter_beacon plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=2.12.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | 12 | dev_dependencies: 13 | flutter_test: 14 | sdk: flutter 15 | 16 | flutter_beacon: 17 | path: ../ 18 | get: ^4.3.6 19 | 20 | flutter: 21 | uses-material-design: true 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter", 9 | "request": "launch", 10 | "type": "dart", 11 | "program": "example/lib/main.dart" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Classes/FBBluetoothStateHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // FBBluetoothStateHandler.h 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 24/08/19. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class FlutterBeaconPlugin; 14 | @interface FBBluetoothStateHandler : NSObject 15 | 16 | @property (strong, nonatomic) FlutterBeaconPlugin* instance; 17 | 18 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /ios/Classes/FBRangingStreamHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // FBRangingStreamHandler.h 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 23/01/19. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class FlutterBeaconPlugin; 14 | @interface FBRangingStreamHandler : NSObject 15 | 16 | @property (strong, nonatomic) FlutterBeaconPlugin* instance; 17 | 18 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /ios/Classes/FBMonitoringStreamHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // FBMonitoringStreamHandler.h 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 23/01/19. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class FlutterBeaconPlugin; 14 | @interface FBMonitoringStreamHandler : NSObject 15 | 16 | @property (strong, nonatomic) FlutterBeaconPlugin* instance; 17 | 18 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /ios/Classes/FBUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // FBUtils.h 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 26/12/18. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @class CLBeacon; 13 | @class CLBeaconRegion; 14 | @interface FBUtils : NSObject 15 | 16 | + (NSDictionary * _Nonnull) dictionaryFromCLBeacon:(CLBeacon*) beacon; 17 | + (NSDictionary * _Nonnull) dictionaryFromCLBeaconRegion:(CLBeaconRegion*) region; 18 | 19 | + (CLBeaconRegion * _Nullable) regionFromDictionary:(NSDictionary*) dict; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /ios/Classes/FBAuthorizationStatusHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // FBAuthorizationStatusHandler.h 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 15/10/19. 6 | // 7 | 8 | #import 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class FlutterBeaconPlugin; 14 | @interface FBAuthorizationStatusHandler : NSObject 15 | 16 | @property (strong, nonatomic) FlutterBeaconPlugin* instance; 17 | 18 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /android/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_beacon 2 | description: Flutter plugin for scanning and transmit as beacon (iBeacon) on Android and iOS. 3 | version: 0.5.1 4 | homepage: https://github.com/alann-maulana/flutter_beacon 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | flutter: ">=1.12.13+hotfix.5" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | flutter: 19 | plugin: 20 | platforms: 21 | android: 22 | package: com.flutterbeacon 23 | pluginClass: FlutterBeaconPlugin 24 | ios: 25 | pluginClass: FlutterBeaconPlugin 26 | -------------------------------------------------------------------------------- /ios/Classes/FlutterBeaconPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class CBCentralManager; 4 | @class CBCentralManagerDelegate; 5 | @interface FlutterBeaconPlugin : NSObject 6 | 7 | @property FlutterEventSink flutterEventSinkRanging; 8 | @property FlutterEventSink flutterEventSinkMonitoring; 9 | @property FlutterEventSink flutterEventSinkBluetooth; 10 | @property FlutterEventSink flutterEventSinkAuthorization; 11 | 12 | - (void) initializeCentralManager; 13 | - (void) initializeLocationManager; 14 | - (void) startRangingBeaconWithCall:(id)arguments; 15 | - (void) stopRangingBeacon; 16 | - (void) startMonitoringBeaconWithCall:(id)arguments; 17 | - (void) stopMonitoringBeacon; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/flutter.yaml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: 6 | master 7 | pull_request: 8 | branches: 9 | master 10 | 11 | jobs: 12 | build: 13 | name: Build & Test on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-java@v1 21 | with: 22 | java-version: '12.x' 23 | - uses: subosito/flutter-action@v1 24 | with: 25 | flutter-version: '2.0.5' 26 | channel: 'stable' 27 | - run: flutter packages get 28 | - run: flutter format --set-exit-if-changed . 29 | - run: flutter analyze . 30 | - run: flutter test 31 | -------------------------------------------------------------------------------- /.github/workflows/pub-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Pub Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | name: Publishing 11 | runs-on: ubuntu-latest 12 | env: 13 | PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-java@v1 17 | with: 18 | java-version: '12.x' 19 | - uses: subosito/flutter-action@v1 20 | with: 21 | flutter-version: '2.0.5' 22 | channel: 'stable' 23 | - name: Documenting package 24 | run: | 25 | echo "FLUTTER_ROOT=$FLUTTER_HOME" >> $GITHUB_ENV 26 | bash scripts/credentials.sh 27 | rm -rf scripts/ 28 | make 29 | make deps 30 | pub global activate dartdoc 31 | make docs 32 | pub publish -f -v 33 | -------------------------------------------------------------------------------- /test/beacon/beacon_broadcast_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_beacon/flutter_beacon.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | main() { 5 | test('main constructor must be equal', () { 6 | final beacon = BeaconBroadcast( 7 | proximityUUID: 'UUID', 8 | major: 1, 9 | minor: 2, 10 | identifier: 'id', 11 | txPower: -58, 12 | advertisingMode: AdvertisingMode.high, 13 | advertisingTxPowerLevel: AdvertisingTxPowerLevel.low, 14 | ); 15 | 16 | expect(beacon.proximityUUID, 'UUID'); 17 | expect(beacon.major, 1); 18 | expect(beacon.minor, 2); 19 | expect(beacon.identifier, 'id'); 20 | expect(beacon.txPower, -58); 21 | expect(beacon.advertisingMode, AdvertisingMode.high); 22 | expect(beacon.advertisingTxPowerLevel, AdvertisingTxPowerLevel.low); 23 | expect(beacon.toJson, isMap); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DARTANALYZER_FLAGS=--fatal-warnings 2 | 3 | build: lib/*dart test/*dart deps 4 | dartanalyzer ${DARTANALYZER_FLAGS} lib/ 5 | dartfmt -n --set-exit-if-changed lib/ test/ 6 | flutter test --coverage --coverage-path ./coverage/lcov.info 7 | 8 | deps: pubspec.yaml 9 | flutter packages get -v 10 | 11 | reformatting: 12 | dartfmt -w lib/ test/ 13 | 14 | build-local: reformatting build 15 | genhtml -o coverage coverage/lcov.info 16 | lcov --list coverage/lcov.info 17 | lcov --summary coverage/lcov.info 18 | open coverage/index.html 19 | 20 | pana: 21 | pana -s path . 22 | 23 | docs: 24 | rm -rf doc 25 | pub global run dartdoc --exclude 'dart:async,dart:collection,dart:convert,dart:core,dart:developer,dart:io,dart:isolate,dart:math,dart:typed_data,dart:ui,dart:html_common,dart:ffi,dart:html,dart:js,dart:js_util' --ignore 'ambiguous-doc-reference' --sdk-dir '${FLUTTER_ROOT}/bin/cache/dart-sdk' 26 | 27 | publish: 28 | pub publish -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.flutterbeacon' 2 | version '1.0-SNAPSHOT' 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.1' 11 | } 12 | } 13 | rootProject.allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | apply plugin: 'com.android.library' 20 | android { 21 | compileSdkVersion 30 22 | 23 | defaultConfig { 24 | minSdkVersion 18 25 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 26 | } 27 | lintOptions { 28 | disable 'InvalidPackage' 29 | } 30 | } 31 | 32 | dependencies { 33 | api 'androidx.legacy:legacy-support-v4:1.0.0' 34 | //api 'org.altbeacon:android-beacon-library:2.16.3' 35 | //api "org.altbeacon:android-beacon-library:2.17.1" 36 | api "org.altbeacon:android-beacon-library:2.19" 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/pub-publish-test.yaml: -------------------------------------------------------------------------------- 1 | name: Pub Publish Test 2 | 3 | on: 4 | push: 5 | branches: 6 | master 7 | pull_request: 8 | branches: 9 | master 10 | 11 | jobs: 12 | build: 13 | name: Publishing Test (Dry Run) 14 | runs-on: ubuntu-latest 15 | env: 16 | PUB_CREDENTIALS: ${{ secrets.PUB_CREDENTIALS }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: '12.x' 22 | - uses: subosito/flutter-action@v1 23 | with: 24 | flutter-version: '2.0.5' 25 | channel: 'stable' 26 | - name: Documenting package 27 | run: | 28 | echo "FLUTTER_ROOT=$FLUTTER_HOME" >> $GITHUB_ENV 29 | bash scripts/credentials.sh 30 | rm -rf scripts/ 31 | make 32 | make deps 33 | pub global activate dartdoc 34 | make docs 35 | pub publish -n -v 36 | -------------------------------------------------------------------------------- /ios/flutter_beacon.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'flutter_beacon' 6 | s.version = '0.3.0' 7 | s.summary = 'Flutter plugin for scanning beacon (iBeacon platform) devices on Android and iOS.' 8 | s.description = <<-DESC 9 | Flutter plugin for scanning beacon (iBeacon platform) devices on Android and iOS. 10 | DESC 11 | s.homepage = 'https://github.com/alann-maulana/flutter_beacon' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Eyro Labs' => 'maulana@cubeacon.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | s.ios.framework = 'CoreLocation', 21 | 'CoreBluetooth' 22 | end 23 | 24 | -------------------------------------------------------------------------------- /lib/beacon/ranging_result.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Class for managing ranging result from scanning iBeacon process. 8 | class RangingResult { 9 | /// The [Region] of ranging result. 10 | final Region region; 11 | 12 | /// The [List] of [Beacon] detected of ranging result by [Region]. 13 | final List beacons; 14 | 15 | /// Constructor for deserialize dynamic json into [RangingResult]. 16 | RangingResult.from(dynamic json) 17 | : region = Region.fromJson(json['region']), 18 | beacons = Beacon.beaconFromArray(json['beacons']); 19 | 20 | /// Return the serializable of this object into [Map]. 21 | dynamic get toJson => { 22 | 'region': region.toJson, 23 | 'beacons': Beacon.beaconArrayToJson(beacons), 24 | }; 25 | 26 | @override 27 | String toString() { 28 | return json.encode(toJson); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /ios/Classes/FBRangingStreamHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // FBRangingStreamHandler.m 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 23/01/19. 6 | // 7 | 8 | #import "FBRangingStreamHandler.h" 9 | #import 10 | 11 | @implementation FBRangingStreamHandler 12 | 13 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance { 14 | if (self = [super init]) { 15 | _instance = instance; 16 | } 17 | 18 | return self; 19 | } 20 | 21 | ///------------------------------------------------------------ 22 | #pragma mark - Flutter Stream Handler 23 | ///------------------------------------------------------------ 24 | 25 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 26 | if (self.instance) { 27 | [self.instance stopRangingBeacon]; 28 | } 29 | return nil; 30 | } 31 | 32 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 33 | if (self.instance) { 34 | self.instance.flutterEventSinkRanging = events; 35 | [self.instance startRangingBeaconWithCall:arguments]; 36 | } 37 | return nil; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /ios/Classes/FBMonitoringStreamHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // FBMonitoringStreamHandler.m 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 23/01/19. 6 | // 7 | 8 | #import "FBMonitoringStreamHandler.h" 9 | #import 10 | 11 | @implementation FBMonitoringStreamHandler 12 | 13 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance { 14 | if (self = [super init]) { 15 | _instance = instance; 16 | } 17 | 18 | return self; 19 | } 20 | 21 | ///------------------------------------------------------------ 22 | #pragma mark - Flutter Stream Handler 23 | ///------------------------------------------------------------ 24 | 25 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 26 | if (self.instance) { 27 | [self.instance stopMonitoringBeacon]; 28 | } 29 | return nil; 30 | } 31 | 32 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 33 | if (self.instance) { 34 | self.instance.flutterEventSinkMonitoring = events; 35 | [self.instance startMonitoringBeaconWithCall:arguments]; 36 | } 37 | return nil; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /ios/Classes/FBBluetoothStateHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // FBBluetoothStateHandler.m 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 24/08/19. 6 | // 7 | 8 | #import "FBBluetoothStateHandler.h" 9 | #import 10 | #import 11 | 12 | @implementation FBBluetoothStateHandler 13 | 14 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance { 15 | if (self = [super init]) { 16 | _instance = instance; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | ///------------------------------------------------------------ 23 | #pragma mark - Flutter Stream Handler 24 | ///------------------------------------------------------------ 25 | 26 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 27 | if (self.instance) { 28 | self.instance.flutterEventSinkBluetooth = nil; 29 | } 30 | return nil; 31 | } 32 | 33 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 34 | // initialize central manager if it itsn't 35 | [self.instance initializeCentralManager]; 36 | 37 | if (self.instance) { 38 | self.instance.flutterEventSinkBluetooth = events; 39 | } 40 | 41 | return nil; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /ios/Classes/FBAuthorizationStatusHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // FBAuthorizationStatusHandler.m 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 15/10/19. 6 | // 7 | 8 | #import "FBAuthorizationStatusHandler.h" 9 | #import 10 | #import 11 | 12 | @implementation FBAuthorizationStatusHandler 13 | 14 | - (instancetype) initWithFlutterBeaconPlugin:(FlutterBeaconPlugin*) instance { 15 | if (self = [super init]) { 16 | _instance = instance; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | ///------------------------------------------------------------ 23 | #pragma mark - Flutter Stream Handler 24 | ///------------------------------------------------------------ 25 | 26 | - (FlutterError * _Nullable)onCancelWithArguments:(id _Nullable)arguments { 27 | if (self.instance) { 28 | self.instance.flutterEventSinkAuthorization = nil; 29 | } 30 | return nil; 31 | } 32 | 33 | - (FlutterError * _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { 34 | // initialize central manager if it itsn't 35 | [self.instance initializeLocationManager]; 36 | 37 | if (self.instance) { 38 | self.instance.flutterEventSinkAuthorization = events; 39 | } 40 | 41 | return nil; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /test/beacon/ranging_result_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_beacon/flutter_beacon.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | main() { 7 | test('MonitoringEventType must equalTo "didEnterRegion', () { 8 | final map = { 9 | 'region': { 10 | 'identifier': 'Cubeacon', 11 | 'proximityUUID': 'CB10023F-A318-3394-4199-A8730C7C1AEC' 12 | }, 13 | 'beacons': [ 14 | { 15 | 'proximityUUID': 'CB10023F-A318-3394-4199-A8730C7C1AEC', 16 | 'major': 1, 17 | 'minor': 1, 18 | 'rssi': -59, 19 | 'accuracy': 1.2, 20 | 'proximity': 'near', 21 | }, 22 | { 23 | 'proximityUUID': 'CB10023F-A318-3394-4199-A8730C7C1AEC', 24 | 'major': 2, 25 | 'minor': 2, 26 | 'rssi': -58, 27 | 'accuracy': 0.8, 28 | 'proximity': 'immediate', 29 | } 30 | ] 31 | }; 32 | final enter = RangingResult.from(map); 33 | 34 | expect(enter.region, isNotNull); 35 | expect(enter.region.identifier, 'Cubeacon'); 36 | expect(enter.region.proximityUUID, 'CB10023F-A318-3394-4199-A8730C7C1AEC'); 37 | expect(enter.beacons, isNotEmpty); 38 | expect(enter.beacons.length, 2); 39 | expect(enter.toJson, map); 40 | expect(enter.toString(), json.encode(map)); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/beacon/region_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_beacon/flutter_beacon.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | main() { 7 | test('constructor test must be equal', () { 8 | final map = { 9 | 'identifier': 'ID', 10 | 'proximityUUID': 'UUID', 11 | 'major': 1, 12 | 'minor': 2, 13 | }; 14 | final region = Region.fromJson(map); 15 | 16 | expect(region.identifier, 'ID'); 17 | expect(region.proximityUUID, 'UUID'); 18 | expect(region.major, 1); 19 | expect(region.minor, 2); 20 | expect(region.toJson, map); 21 | expect(region.toString(), json.encode(map)); 22 | }); 23 | 24 | test('two regions must be equal', () { 25 | final region1 = Region.fromJson({ 26 | 'identifier': 'ID', 27 | 'proximityUUID': 'UUID', 28 | 'major': '1', 29 | 'minor': '2', 30 | }); 31 | final region2 = Region( 32 | identifier: 'ID', 33 | proximityUUID: 'UUID', 34 | major: 1, 35 | minor: 2, 36 | ); 37 | 38 | expect(region1.identifier, region2.identifier); 39 | expect(region1.proximityUUID, region2.proximityUUID); 40 | expect(region1.major, region2.major); 41 | expect(region1.minor, region2.minor); 42 | expect(region1 == region2, isTrue); 43 | expect(region1.hashCode == region2.hashCode, isTrue); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_beacon_example/controller/requirement_state_controller.dart'; 4 | import 'package:flutter_beacon_example/view/home_page.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | void main() { 8 | runApp(MainApp()); 9 | } 10 | 11 | class MainApp extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | Get.put(RequirementStateController()); 15 | 16 | final themeData = Theme.of(context); 17 | final primary = Colors.blue; 18 | 19 | return GetMaterialApp( 20 | theme: ThemeData( 21 | brightness: Brightness.light, 22 | primarySwatch: primary, 23 | appBarTheme: themeData.appBarTheme.copyWith( 24 | brightness: Brightness.light, 25 | elevation: 0.5, 26 | color: Colors.white, 27 | actionsIconTheme: themeData.primaryIconTheme.copyWith( 28 | color: primary, 29 | ), 30 | iconTheme: themeData.primaryIconTheme.copyWith( 31 | color: primary, 32 | ), 33 | textTheme: themeData.primaryTextTheme.copyWith( 34 | headline6: themeData.textTheme.headline6?.copyWith( 35 | color: primary, 36 | ), 37 | ), 38 | ), 39 | ), 40 | darkTheme: ThemeData( 41 | brightness: Brightness.dark, 42 | primarySwatch: primary, 43 | ), 44 | home: HomePage(), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | ios/Flutter/Flutter.podspec -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ### Initializing Library 2 | 3 | ```dart 4 | try { 5 | await flutterBeacon.initializeScanning; 6 | } on PlatformException catch(e) { 7 | // library failed to initialize, check code and message 8 | } 9 | ``` 10 | 11 | ### Ranging beacons 12 | 13 | ```dart 14 | final regions = []; 15 | 16 | if (Platform.isIOS) { 17 | regions.add(Region( 18 | identifier: 'Apple Airlocate', 19 | proximityUUID: 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0')); 20 | } else { 21 | // android platform, it can ranging out of beacon that filter all of Proximity UUID 22 | regions.add(Region(identifier: 'com.beacon')); 23 | } 24 | 25 | // to start ranging beacons 26 | _streamRanging = flutterBeacon.ranging(regions).listen((RangingResult result) { 27 | // result contains a region and list of beacons found 28 | // list can be empty if no matching beacons were found in range 29 | }); 30 | 31 | // to stop ranging beacons 32 | _streamRanging.cancel(); 33 | ``` 34 | 35 | ### Monitoring beacons 36 | 37 | ```dart 38 | final regions = []; 39 | 40 | if (Platform.isIOS) { 41 | regions.add(Region( 42 | identifier: 'Apple Airlocate', 43 | proximityUUID: 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0')); 44 | } else { 45 | // android platform, it can ranging out of beacon that filter all of Proximity UUID 46 | regions.add(Region(identifier: 'com.beacon')); 47 | } 48 | 49 | // to start monitoring beacons 50 | _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { 51 | // result contains a region, event type and event state 52 | }); 53 | 54 | // to stop monitoring beacons 55 | _streamMonitoring.cancel(); 56 | ``` 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.5.1] 2 | * Migration the example to null safety and to Android v2 embedding 3 | * Update Android-Beacon-Library to version [2.19] with added support to Android 12 (https://github.com/AltBeacon/android-beacon-library/tree/2.19) 4 | * Update README.md with new Android permission requirement for location (SDK 29+, Android 10, 11) 5 | 6 | ## [0.5.0] 7 | * Migration to null safety 8 | 9 | ## [0.4.0] 10 | * Update to Android v2 embedding 11 | * Add broadcast Beacon feature 12 | * Improve example app 13 | 14 | ## [0.3.0] 15 | * Update Android-Beacon-Library to version [2.16.3](https://github.com/AltBeacon/android-beacon-library/tree/2.16.3) 16 | * Add `BluetoothState` event channel 17 | * Add `AuthorizationStatus` event channel [iOS only] 18 | * Add manual check for `AuthorizationStatus`, `BluetoothState`, Location Service. 19 | * Add opener settings for Bluetooth [Android], Location [Android] and Application [iOS]. 20 | * A lot of improvements 21 | 22 | ## [0.2.4] 23 | 24 | * Fix bugs 25 | * Add `close` scanning API 26 | 27 | ## [0.2.3] 28 | 29 | * Update gradle build to latest version 30 | * Update stop 31 | 32 | ## [0.2.2] 33 | 34 | * Update gradle to latest version 35 | * Update Android-Beacon-Library to version [2.16.1](https://github.com/AltBeacon/android-beacon-library/tree/2.16.1) 36 | 37 | ## [0.2.1] 38 | 39 | * Fix crash ranging when region not set for Android 40 | 41 | ## [0.2.0] 42 | 43 | * Migrate to AndroidX 44 | 45 | ## [0.1.1] 46 | 47 | * Fix region ranging for Android 48 | 49 | ## [0.1.0] 50 | 51 | * Updating library docs 52 | * Adding example docs 53 | 54 | ## [0.0.2] 55 | 56 | * Adding Dart Docs 57 | 58 | ## [0.0.1] 59 | 60 | * Initial release 61 | * Ranging iBeacons for iOS and Android -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/coverage-report.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage Coverage Report 2 | 3 | on: 4 | push: 5 | branches: 6 | master 7 | pull_request: 8 | branches: 9 | master 10 | 11 | jobs: 12 | codecov_io: 13 | name: "Codecov.io" 14 | runs-on: ubuntu-latest 15 | env: 16 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: '12.x' 22 | - uses: subosito/flutter-action@v1 23 | with: 24 | flutter-version: '2.0.5' 25 | channel: 'stable' 26 | - name: Generate coverage report 27 | run: flutter test --coverage --coverage-path ./coverage/lcov.info 28 | - name: Send to codecov.io 29 | run: bash <(curl -s https://codecov.io/bash) 30 | - run: ls -al ./coverage 31 | - name: "Upload coverage" 32 | uses: actions/upload-artifact@v1 33 | with: 34 | name: coverage 35 | path: ./coverage 36 | 37 | coveralls_io: 38 | name: "Coveralls.io" 39 | needs: codecov_io 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: "Download coverage" 44 | uses: actions/download-artifact@v1 45 | with: 46 | name: coverage 47 | path: ./coverage 48 | - name: "Setup ruby" 49 | uses: actions/setup-ruby@v1 50 | with: 51 | ruby-version: '2.6' 52 | - run: gem install coveralls-lcov 53 | - run: cat ./coverage/lcov.info 54 | - name: Send to coveralls.io 55 | run: coveralls-lcov --verbose --repo-token $COVERALLS_TOKEN coverage/lcov.info 56 | env: 57 | COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} 58 | -------------------------------------------------------------------------------- /example/lib/controller/requirement_state_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_beacon/flutter_beacon.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | class RequirementStateController extends GetxController { 5 | var bluetoothState = BluetoothState.stateOff.obs; 6 | var authorizationStatus = AuthorizationStatus.notDetermined.obs; 7 | var locationService = false.obs; 8 | 9 | var _startBroadcasting = false.obs; 10 | var _startScanning = false.obs; 11 | var _pauseScanning = false.obs; 12 | 13 | bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn; 14 | bool get authorizationStatusOk => 15 | authorizationStatus.value == AuthorizationStatus.allowed || 16 | authorizationStatus.value == AuthorizationStatus.always; 17 | bool get locationServiceEnabled => locationService.value; 18 | 19 | updateBluetoothState(BluetoothState state) { 20 | bluetoothState.value = state; 21 | } 22 | 23 | updateAuthorizationStatus(AuthorizationStatus status) { 24 | authorizationStatus.value = status; 25 | } 26 | 27 | updateLocationService(bool flag) { 28 | locationService.value = flag; 29 | } 30 | 31 | startBroadcasting() { 32 | _startBroadcasting.value = true; 33 | } 34 | 35 | stopBroadcasting() { 36 | _startBroadcasting.value = false; 37 | } 38 | 39 | startScanning() { 40 | _startScanning.value = true; 41 | _pauseScanning.value = false; 42 | } 43 | 44 | pauseScanning() { 45 | _startScanning.value = false; 46 | _pauseScanning.value = true; 47 | } 48 | 49 | Stream get startBroadcastStream { 50 | return _startBroadcasting.stream; 51 | } 52 | 53 | Stream get startStream { 54 | return _startScanning.stream; 55 | } 56 | 57 | Stream get pauseStream { 58 | return _pauseScanning.stream; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.flutter_beacon_example" 38 | minSdkVersion 18 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Flutter Beacon 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Flutter Beacon 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | NSBluetoothAlwaysUsageDescription 28 | This app need to scan beacons. 29 | NSLocationAlwaysAndWhenInUseUsageDescription 30 | This app need to scan beacons. 31 | NSLocationAlwaysUsageDescription 32 | This app need to scan beacons. 33 | NSLocationWhenInUseUsageDescription 34 | This app need to scan beacons. 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/beacon/beacon_broadcast.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Class for managing Beacon Broadcast object. 8 | class BeaconBroadcast { 9 | /// The unique identifier of region. 10 | final String? identifier; 11 | 12 | /// The proximity UUID of beacon. 13 | final String proximityUUID; 14 | 15 | /// The major value of beacon. 16 | final int major; 17 | 18 | /// The minor value of beacon. 19 | final int minor; 20 | 21 | /// The txPower value of beacon. Specify null to use the default value for the device. 22 | final int? txPower; 23 | 24 | final AdvertisingMode? advertisingMode; 25 | 26 | final AdvertisingTxPowerLevel? advertisingTxPowerLevel; 27 | 28 | BeaconBroadcast({ 29 | this.identifier = 'com.flutterBeacon', 30 | required this.proximityUUID, 31 | required this.major, 32 | required this.minor, 33 | this.txPower, 34 | this.advertisingMode = AdvertisingMode.low, 35 | this.advertisingTxPowerLevel = AdvertisingTxPowerLevel.high, 36 | }) { 37 | if (Platform.isAndroid) { 38 | assert(advertisingMode != null); 39 | assert(advertisingTxPowerLevel != null); 40 | } else if (Platform.isIOS) { 41 | assert(identifier != null); 42 | } 43 | } 44 | 45 | /// Serialize current instance object into [Map]. 46 | dynamic get toJson { 47 | final map = { 48 | 'proximityUUID': proximityUUID, 49 | 'major': major, 50 | 'minor': minor, 51 | 'txPower': txPower, 52 | }; 53 | 54 | if (advertisingMode != null) { 55 | map['advertisingMode'] = advertisingMode!.index; 56 | } 57 | 58 | if (advertisingTxPowerLevel != null) { 59 | map['advertisingTxPowerLevel'] = advertisingTxPowerLevel!.index; 60 | } 61 | 62 | if (identifier != null) { 63 | map['identifier'] = identifier!; 64 | } 65 | 66 | return map; 67 | } 68 | } 69 | 70 | enum AdvertisingMode { low, mid, high } 71 | 72 | enum AdvertisingTxPowerLevel { ultraLow, low, mid, high } 73 | -------------------------------------------------------------------------------- /flutter_beacon.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/beacon/authorization_status_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_beacon/flutter_beacon.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | test('authorization initialization', () { 6 | final authorizationStatus = AuthorizationStatus.init( 7 | 'VALUE', 8 | isAndroid: true, 9 | isIOS: true, 10 | ); 11 | expect(authorizationStatus.value, 'VALUE'); 12 | expect(authorizationStatus.isAndroid, isTrue); 13 | expect(authorizationStatus.isIOS, isTrue); 14 | }); 15 | 16 | test('authorization must be equal', () { 17 | final statusA = AuthorizationStatus.init( 18 | 'VALUE', 19 | isAndroid: false, 20 | isIOS: true, 21 | ); 22 | final statusB = AuthorizationStatus.init( 23 | 'VALUE', 24 | isAndroid: false, 25 | isIOS: true, 26 | ); 27 | expect(statusA, statusB); 28 | expect(statusA.hashCode, statusB.hashCode); 29 | expect(statusA.value, statusB.value); 30 | expect(statusA.isAndroid, statusB.isAndroid); 31 | expect(statusA.isIOS, statusB.isIOS); 32 | expect(statusA.toString(), statusB.toString()); 33 | }); 34 | 35 | test('authorization value', () { 36 | expect(AuthorizationStatus.allowed.value, 'ALLOWED'); 37 | expect(AuthorizationStatus.always.value, 'ALWAYS'); 38 | expect(AuthorizationStatus.whenInUse.value, 'WHEN_IN_USE'); 39 | expect(AuthorizationStatus.denied.value, 'DENIED'); 40 | expect(AuthorizationStatus.restricted.value, 'RESTRICTED'); 41 | expect(AuthorizationStatus.notDetermined.value, 'NOT_DETERMINED'); 42 | }); 43 | 44 | test('parse authorization value', () { 45 | expect(AuthorizationStatus.parse('ALLOWED'), AuthorizationStatus.allowed); 46 | expect(AuthorizationStatus.parse('ALWAYS'), AuthorizationStatus.always); 47 | expect(AuthorizationStatus.parse('WHEN_IN_USE'), 48 | AuthorizationStatus.whenInUse); 49 | expect(AuthorizationStatus.parse('DENIED'), AuthorizationStatus.denied); 50 | expect(AuthorizationStatus.parse('RESTRICTED'), 51 | AuthorizationStatus.restricted); 52 | expect(AuthorizationStatus.parse('NOT_DETERMINED'), 53 | AuthorizationStatus.notDetermined); 54 | expect(() => AuthorizationStatus.parse('null'), throwsException); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/beacon/monitoring_result_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_beacon/flutter_beacon.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | main() { 5 | test('MonitoringEventType must equalTo "didEnterRegion', () { 6 | final map = { 7 | 'region': { 8 | 'identifier': 'id', 9 | 'proximityUUID': 'UUID', 10 | }, 11 | 'event': 'didEnterRegion', 12 | }; 13 | final enter = MonitoringResult.from(map); 14 | 15 | expect(enter.region, isNotNull); 16 | expect(enter.region.identifier, 'id'); 17 | expect(enter.region.proximityUUID, 'UUID'); 18 | expect(enter.monitoringEventType, MonitoringEventType.didEnterRegion); 19 | expect(enter.toJson, map); 20 | }); 21 | 22 | test('MonitoringEventType must equalTo "didExitRegion', () { 23 | final map = { 24 | 'region': { 25 | 'identifier': 'id', 26 | 'proximityUUID': 'UUID', 27 | }, 28 | 'event': 'didExitRegion', 29 | }; 30 | final enter = MonitoringResult.from(map); 31 | 32 | expect(enter.region, isNotNull); 33 | expect(enter.region.identifier, 'id'); 34 | expect(enter.region.proximityUUID, 'UUID'); 35 | expect(enter.monitoringEventType, MonitoringEventType.didExitRegion); 36 | expect(enter.toJson, map); 37 | }); 38 | 39 | test('MonitoringEventType must equalTo "didDetermineStateForRegion', () { 40 | final map = { 41 | 'region': { 42 | 'identifier': 'id', 43 | 'proximityUUID': 'UUID', 44 | }, 45 | 'state': 'unknown', 46 | 'event': 'didDetermineStateForRegion', 47 | }; 48 | final enter = MonitoringResult.from(map); 49 | 50 | expect(enter.region, isNotNull); 51 | expect(enter.region.identifier, 'id'); 52 | expect(enter.region.proximityUUID, 'UUID'); 53 | expect(enter.monitoringEventType, 54 | MonitoringEventType.didDetermineStateForRegion); 55 | expect(enter.monitoringState, MonitoringState.unknown); 56 | expect(enter.toJson, map); 57 | }); 58 | 59 | test('MonitoringResult must throw', () { 60 | final map = { 61 | 'region': { 62 | 'identifier': 'id', 63 | 'proximityUUID': 'UUID', 64 | }, 65 | 'state': 'unknown', 66 | 'event': 'null', 67 | }; 68 | 69 | expect(() => MonitoringResult.from(map), throwsException); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/beacon/bluetooth_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_beacon/flutter_beacon.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | test('bluetooth state initialization', () { 6 | final bluetoothState = BluetoothState.init( 7 | 'VALUE', 8 | isAndroid: true, 9 | isIOS: true, 10 | ); 11 | expect(bluetoothState.value, 'VALUE'); 12 | expect(bluetoothState.isAndroid, isTrue); 13 | expect(bluetoothState.isIOS, isTrue); 14 | }); 15 | 16 | test('bluetooth state must be equal', () { 17 | final stateA = BluetoothState.init( 18 | 'VALUE', 19 | isAndroid: true, 20 | isIOS: false, 21 | ); 22 | final stateB = BluetoothState.init( 23 | 'VALUE', 24 | isAndroid: true, 25 | isIOS: false, 26 | ); 27 | expect(stateA, stateB); 28 | expect(stateA.hashCode, stateB.hashCode); 29 | expect(stateA.value, stateB.value); 30 | expect(stateA.isAndroid, stateB.isAndroid); 31 | expect(stateA.isIOS, stateB.isIOS); 32 | expect(stateA.toString(), stateB.toString()); 33 | }); 34 | 35 | test('bluetooth state value', () { 36 | expect(BluetoothState.stateOff.value, 'STATE_OFF'); 37 | expect(BluetoothState.stateTurningOff.value, 'STATE_TURNING_OFF'); 38 | expect(BluetoothState.stateOn.value, 'STATE_ON'); 39 | expect(BluetoothState.stateTurningOn.value, 'STATE_TURNING_ON'); 40 | expect(BluetoothState.stateUnknown.value, 'STATE_UNKNOWN'); 41 | expect(BluetoothState.stateResetting.value, 'STATE_RESETTING'); 42 | expect(BluetoothState.stateUnsupported.value, 'STATE_UNSUPPORTED'); 43 | expect(BluetoothState.stateUnauthorized.value, 'STATE_UNAUTHORIZED'); 44 | }); 45 | 46 | test('parse bluetooth state value', () { 47 | expect(BluetoothState.parse('STATE_OFF'), BluetoothState.stateOff); 48 | expect(BluetoothState.parse('STATE_TURNING_OFF'), 49 | BluetoothState.stateTurningOff); 50 | expect(BluetoothState.parse('STATE_ON'), BluetoothState.stateOn); 51 | expect(BluetoothState.parse('STATE_TURNING_ON'), 52 | BluetoothState.stateTurningOn); 53 | expect(BluetoothState.parse('STATE_UNKNOWN'), BluetoothState.stateUnknown); 54 | expect( 55 | BluetoothState.parse('STATE_RESETTING'), BluetoothState.stateResetting); 56 | expect(BluetoothState.parse('STATE_UNSUPPORTED'), 57 | BluetoothState.stateUnsupported); 58 | expect(BluetoothState.parse('STATE_UNAUTHORIZED'), 59 | BluetoothState.stateUnauthorized); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutterbeacon/FlutterBluetoothStateReceiver.java: -------------------------------------------------------------------------------- 1 | package com.flutterbeacon; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothManager; 6 | import android.content.BroadcastReceiver; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.IntentFilter; 10 | 11 | import io.flutter.plugin.common.EventChannel; 12 | 13 | class FlutterBluetoothStateReceiver extends BroadcastReceiver implements EventChannel.StreamHandler { 14 | private final Context context; 15 | private EventChannel.EventSink eventSink; 16 | 17 | public FlutterBluetoothStateReceiver(Context context) { 18 | this.context = context; 19 | } 20 | 21 | @Override 22 | public void onReceive(Context context, Intent intent) { 23 | if (eventSink == null) return; 24 | final String action = intent.getAction(); 25 | 26 | if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { 27 | final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 28 | sendState(state); 29 | } 30 | } 31 | 32 | private void sendState(int state) { 33 | switch (state) { 34 | case BluetoothAdapter.STATE_OFF: 35 | eventSink.success("STATE_OFF"); 36 | break; 37 | case BluetoothAdapter.STATE_TURNING_OFF: 38 | eventSink.success("STATE_TURNING_OFF"); 39 | break; 40 | case BluetoothAdapter.STATE_ON: 41 | eventSink.success("STATE_ON"); 42 | break; 43 | case BluetoothAdapter.STATE_TURNING_ON: 44 | eventSink.success("STATE_TURNING_ON"); 45 | break; 46 | default: 47 | eventSink.error("BLUETOOTH_STATE", "invalid bluetooth adapter state", null); 48 | break; 49 | } 50 | } 51 | 52 | @SuppressLint("MissingPermission") 53 | @Override 54 | public void onListen(Object o, EventChannel.EventSink eventSink) { 55 | int state = BluetoothAdapter.STATE_OFF; 56 | 57 | BluetoothManager bluetoothManager = (BluetoothManager) 58 | context.getSystemService(Context.BLUETOOTH_SERVICE); 59 | if (bluetoothManager != null) { 60 | BluetoothAdapter adapter = bluetoothManager.getAdapter(); 61 | if (adapter != null) { 62 | state = adapter.getState(); 63 | } 64 | } 65 | this.eventSink = eventSink; 66 | this.sendState(state); 67 | 68 | IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 69 | context.registerReceiver(this, filter); 70 | } 71 | 72 | @Override 73 | public void onCancel(Object o) { 74 | context.unregisterReceiver(this); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ios/Classes/FBUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // FBUtils.m 3 | // flutter_beacon 4 | // 5 | // Created by Alann Maulana on 26/12/18. 6 | // 7 | 8 | #import "FBUtils.h" 9 | #import 10 | 11 | @implementation FBUtils 12 | 13 | + (NSDictionary * _Nonnull) dictionaryFromCLBeacon:(CLBeacon*) beacon { 14 | NSString *proximity; 15 | switch (beacon.proximity) { 16 | case CLProximityUnknown: 17 | proximity = @"unknown"; 18 | break; 19 | case CLProximityImmediate: 20 | proximity = @"immediate"; 21 | break; 22 | case CLProximityNear: 23 | proximity = @"near"; 24 | break; 25 | case CLProximityFar: 26 | proximity = @"far"; 27 | break; 28 | } 29 | 30 | NSNumber *rssi = [NSNumber numberWithInteger:beacon.rssi]; 31 | return @{ 32 | @"proximityUUID": [beacon.proximityUUID UUIDString], 33 | @"major": beacon.major, 34 | @"minor": beacon.minor, 35 | @"rssi": rssi, 36 | @"accuracy": [NSString stringWithFormat:@"%.2f", beacon.accuracy], 37 | @"proximity": proximity 38 | }; 39 | } 40 | 41 | + (NSDictionary * _Nonnull) dictionaryFromCLBeaconRegion:(CLBeaconRegion*) region { 42 | id major = region.major; 43 | if (!major) { 44 | major = [NSNull null]; 45 | } 46 | id minor = region.minor; 47 | if (!minor) { 48 | minor = [NSNull null]; 49 | } 50 | 51 | return @{ 52 | @"identifier": region.identifier, 53 | @"proximityUUID": [region.proximityUUID UUIDString], 54 | @"major": major, 55 | @"minor": minor, 56 | }; 57 | } 58 | 59 | + (CLBeaconRegion * _Nullable) regionFromDictionary:(NSDictionary*) dict { 60 | NSString *identifier = dict[@"identifier"]; 61 | NSString *proximityUUID = dict[@"proximityUUID"]; 62 | NSNumber *major = dict[@"major"]; 63 | NSNumber *minor = dict[@"minor"]; 64 | 65 | CLBeaconRegion *region = nil; 66 | if (proximityUUID && major && minor) { 67 | region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:proximityUUID] major:[major intValue] minor:[minor intValue] identifier:identifier]; 68 | } else if (proximityUUID && major) { 69 | region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:proximityUUID] major:[major intValue] identifier:identifier]; 70 | } else if (proximityUUID) { 71 | region = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:proximityUUID] identifier:identifier]; 72 | } 73 | 74 | return region; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /lib/beacon/region.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Class for managing for ranging and monitoring region scanning. 8 | class Region { 9 | /// The unique identifier of region. 10 | final String identifier; 11 | 12 | /// The proximity UUID of region. 13 | /// 14 | /// For iOS, this value can not be null. 15 | final String? proximityUUID; 16 | 17 | /// The major number of region. 18 | /// 19 | /// For both Android and iOS, this value can be null. 20 | final int? major; 21 | 22 | /// The minor number of region. 23 | /// 24 | /// For both Android and iOS, this value can be null. 25 | final int? minor; 26 | 27 | /// Constructor for creating [Region] object. 28 | /// 29 | /// The [proximityUUID] must not be null when [Platform.isIOS] 30 | Region({ 31 | required this.identifier, 32 | this.proximityUUID, 33 | this.major, 34 | this.minor, 35 | }) { 36 | if (Platform.isIOS) { 37 | assert( 38 | proximityUUID != null, 39 | 'Scanning beacon for iOS must provided proximityUUID', 40 | ); 41 | } 42 | } 43 | 44 | /// Constructor for deserialize json [Map] into [Region] object. 45 | Region.fromJson(dynamic json) 46 | : this( 47 | identifier: json['identifier'], 48 | proximityUUID: json['proximityUUID'], 49 | major: _parseMajorMinor(json['major']), 50 | minor: _parseMajorMinor(json['minor']), 51 | ); 52 | 53 | /// Return the serializable of this object into [Map]. 54 | dynamic get toJson { 55 | final map = { 56 | 'identifier': identifier, 57 | }; 58 | 59 | if (proximityUUID != null) { 60 | map['proximityUUID'] = proximityUUID; 61 | } 62 | 63 | if (major != null) { 64 | map['major'] = major; 65 | } 66 | 67 | if (minor != null) { 68 | map['minor'] = minor; 69 | } 70 | 71 | return map; 72 | } 73 | 74 | @override 75 | bool operator ==(Object other) => 76 | identical(this, other) || 77 | other is Region && 78 | runtimeType == other.runtimeType && 79 | identifier == other.identifier; 80 | 81 | @override 82 | int get hashCode => identifier.hashCode; 83 | 84 | static int? _parseMajorMinor(dynamic number) { 85 | if (number is num) { 86 | return number.toInt(); 87 | } 88 | 89 | if (number is String) { 90 | return int.tryParse(number); 91 | } 92 | 93 | return null; 94 | } 95 | 96 | @override 97 | String toString() { 98 | return json.encode(toJson); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/beacon/monitoring_result.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Enum for defining monitoring event type. 8 | enum MonitoringEventType { 9 | didEnterRegion, 10 | didExitRegion, 11 | didDetermineStateForRegion 12 | } 13 | 14 | /// Enum for defining monitoring state 15 | enum MonitoringState { inside, outside, unknown } 16 | 17 | /// Class for managing monitoring result from scanning iBeacon process. 18 | class MonitoringResult { 19 | /// The [MonitoringEventType] of monitoring result 20 | final MonitoringEventType monitoringEventType; 21 | 22 | /// The [MonitoringState] of monitoring result 23 | /// 24 | /// This value is not null when [monitoringEventType] is [MonitoringEventType.didDetermineStateForRegion] 25 | final MonitoringState? monitoringState; 26 | 27 | /// The [Region] of ranging result. 28 | final Region region; 29 | 30 | /// Constructor for deserialize dynamic json into [MonitoringResult]. 31 | MonitoringResult.from(dynamic json) 32 | : this.monitoringEventType = _parseMonitoringEventType(json['event']), 33 | this.monitoringState = _parseMonitoringState(json['state']), 34 | this.region = Region.fromJson(json['region']); 35 | 36 | /// Parsing dynamic state into [MonitoringState]. 37 | static MonitoringState? _parseMonitoringState(dynamic state) { 38 | if (!(state is String)) { 39 | return null; 40 | } 41 | 42 | if (state.toLowerCase() == 'inside') { 43 | return MonitoringState.inside; 44 | } else if (state.toLowerCase() == 'outside') { 45 | return MonitoringState.outside; 46 | } else if (state.toLowerCase() == 'unknown') { 47 | return MonitoringState.unknown; 48 | } 49 | 50 | return null; 51 | } 52 | 53 | /// Parsing dynamic event into [MonitoringEventType] 54 | static MonitoringEventType _parseMonitoringEventType(dynamic event) { 55 | if (event == 'didEnterRegion') { 56 | return MonitoringEventType.didEnterRegion; 57 | } else if (event == 'didExitRegion') { 58 | return MonitoringEventType.didExitRegion; 59 | } else if (event == 'didDetermineStateForRegion') { 60 | return MonitoringEventType.didDetermineStateForRegion; 61 | } 62 | 63 | throw Exception('invalid monitoring event type $event'); 64 | } 65 | 66 | /// Return the serializable of this object into [Map]. 67 | dynamic get toJson { 68 | final map = { 69 | 'event': monitoringEventType.toString().split('.').last, 70 | 'region': region.toJson, 71 | }; 72 | 73 | if (monitoringState != null) { 74 | map['state'] = monitoringState.toString().split('.').last; 75 | } 76 | 77 | return map; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /android/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutterbeacon/FlutterBeaconBroadcast.java: -------------------------------------------------------------------------------- 1 | package com.flutterbeacon; 2 | 3 | import android.app.Activity; 4 | import android.bluetooth.le.AdvertiseCallback; 5 | import android.bluetooth.le.AdvertiseSettings; 6 | import android.os.Build; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import org.altbeacon.beacon.Beacon; 12 | import org.altbeacon.beacon.BeaconParser; 13 | import org.altbeacon.beacon.BeaconTransmitter; 14 | 15 | import java.util.Map; 16 | 17 | import io.flutter.plugin.common.MethodChannel; 18 | 19 | class FlutterBeaconBroadcast { 20 | private static final String TAG = FlutterBeaconBroadcast.class.getSimpleName(); 21 | private final BeaconTransmitter beaconTransmitter; 22 | 23 | FlutterBeaconBroadcast(Activity activity, BeaconParser iBeaconLayout) { 24 | this.beaconTransmitter = new BeaconTransmitter(activity, iBeaconLayout); 25 | } 26 | 27 | void isBroadcasting(@NonNull MethodChannel.Result result) { 28 | result.success(beaconTransmitter.isStarted()); 29 | } 30 | 31 | void stopBroadcast(@NonNull MethodChannel.Result result) { 32 | beaconTransmitter.stopAdvertising(); 33 | result.success(true); 34 | } 35 | 36 | @SuppressWarnings("rawtypes") 37 | void startBroadcast(Object arguments, @NonNull final MethodChannel.Result result) { 38 | if (!(arguments instanceof Map)) { 39 | result.error("Broadcast", "Invalid parameter", null); 40 | return; 41 | } 42 | 43 | Map map = (Map) arguments; 44 | final Beacon beacon = FlutterBeaconUtils.beaconFromMap(map); 45 | 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 47 | Object advertisingMode = map.get("advertisingMode"); 48 | if (advertisingMode instanceof Integer) { 49 | beaconTransmitter.setAdvertiseMode((Integer) advertisingMode); 50 | } 51 | Object advertisingTxPowerLevel = map.get("advertisingTxPowerLevel"); 52 | if (advertisingTxPowerLevel instanceof Integer) { 53 | beaconTransmitter.setAdvertiseTxPowerLevel((Integer) advertisingTxPowerLevel); 54 | } 55 | beaconTransmitter.startAdvertising(beacon, new AdvertiseCallback() { 56 | @Override 57 | public void onStartSuccess(AdvertiseSettings settingsInEffect) { 58 | Log.d(TAG, "Start broadcasting = " + beacon); 59 | result.success(true); 60 | } 61 | 62 | @Override 63 | public void onStartFailure(int errorCode) { 64 | String error = "FEATURE_UNSUPPORTED"; 65 | if (errorCode == ADVERTISE_FAILED_DATA_TOO_LARGE) { 66 | error = "DATA_TOO_LARGE"; 67 | } else if (errorCode == ADVERTISE_FAILED_TOO_MANY_ADVERTISERS) { 68 | error = "TOO_MANY_ADVERTISERS"; 69 | } else if (errorCode == ADVERTISE_FAILED_ALREADY_STARTED) { 70 | error = "ALREADY_STARTED"; 71 | } else if (errorCode == ADVERTISE_FAILED_INTERNAL_ERROR) { 72 | error = "INTERNAL_ERROR"; 73 | } 74 | Log.e(TAG, error); 75 | result.error("Broadcast", error, null); 76 | } 77 | }); 78 | } else { 79 | Log.e(TAG, "FEATURE_UNSUPPORTED"); 80 | result.error("Broadcast", "FEATURE_UNSUPPORTED", null); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 17 | 21 | 28 | 32 | 36 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/beacon/authorization_status.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Enum class for showing status about authorization. 8 | class AuthorizationStatus { 9 | /// The defined [String] value of the authorization status. 10 | final String value; 11 | 12 | /// This will `true` only if this authorization status suit Android system. 13 | final bool isAndroid; 14 | 15 | /// This will `true` only if this authorization status suit iOS system. 16 | final bool isIOS; 17 | 18 | @visibleForTesting 19 | const AuthorizationStatus.init( 20 | this.value, { 21 | this.isAndroid = false, 22 | this.isIOS = false, 23 | }); 24 | 25 | @visibleForTesting 26 | factory AuthorizationStatus.parse(String value) { 27 | switch (value) { 28 | case 'ALLOWED': 29 | return allowed; 30 | case 'ALWAYS': 31 | return always; 32 | case 'WHEN_IN_USE': 33 | return whenInUse; 34 | case 'DENIED': 35 | return denied; 36 | case 'RESTRICTED': 37 | return restricted; 38 | case 'NOT_DETERMINED': 39 | return notDetermined; 40 | } 41 | 42 | throw Exception('invalid authorization status $value'); 43 | } 44 | 45 | /// Shows that user allowed the authorization. 46 | /// 47 | /// Only for Android 48 | static const AuthorizationStatus allowed = AuthorizationStatus.init( 49 | 'ALLOWED', 50 | isAndroid: true, 51 | isIOS: false, 52 | ); 53 | 54 | /// Shows that user always authorize app. 55 | /// 56 | /// Only for iOS 57 | static const AuthorizationStatus always = AuthorizationStatus.init( 58 | 'ALWAYS', 59 | isAndroid: false, 60 | isIOS: true, 61 | ); 62 | 63 | /// Shows that user authorize when in use app. 64 | /// 65 | /// Only for iOS 66 | static const AuthorizationStatus whenInUse = AuthorizationStatus.init( 67 | 'WHEN_IN_USE', 68 | isAndroid: false, 69 | isIOS: true, 70 | ); 71 | 72 | /// Shows that user denied authorization request. 73 | static const AuthorizationStatus denied = AuthorizationStatus.init( 74 | 'DENIED', 75 | isAndroid: true, 76 | isIOS: true, 77 | ); 78 | 79 | /// Shows that authorization has been restricted by system. 80 | /// 81 | /// Only for iOS 82 | static const AuthorizationStatus restricted = AuthorizationStatus.init( 83 | 'RESTRICTED', 84 | isAndroid: false, 85 | isIOS: true, 86 | ); 87 | 88 | /// Shows that authorization has not been determined by user. 89 | /// 90 | static const AuthorizationStatus notDetermined = AuthorizationStatus.init( 91 | 'NOT_DETERMINED', 92 | isAndroid: true, 93 | isIOS: true, 94 | ); 95 | 96 | @override 97 | bool operator ==(Object other) => 98 | identical(this, other) || 99 | other is AuthorizationStatus && 100 | runtimeType == other.runtimeType && 101 | value == other.value && 102 | isAndroid == other.isAndroid && 103 | isIOS == other.isIOS; 104 | 105 | @override 106 | int get hashCode => value.hashCode ^ isAndroid.hashCode ^ isIOS.hashCode; 107 | 108 | @override 109 | String toString() { 110 | return value; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/beacon/bluetooth_state.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Enum class for showing state about bluetooth. 8 | class BluetoothState { 9 | @visibleForTesting 10 | const BluetoothState.init( 11 | this.value, { 12 | this.isAndroid = true, 13 | this.isIOS = true, 14 | }); 15 | 16 | @visibleForTesting 17 | factory BluetoothState.parse(dynamic state) { 18 | switch (state) { 19 | case "STATE_OFF": 20 | return stateOff; 21 | case "STATE_TURNING_OFF": 22 | return stateTurningOff; 23 | case "STATE_ON": 24 | return stateOn; 25 | case "STATE_TURNING_ON": 26 | return stateTurningOn; 27 | case "STATE_UNKNOWN": 28 | return stateUnknown; 29 | case "STATE_RESETTING": 30 | return stateResetting; 31 | case "STATE_UNSUPPORTED": 32 | return stateUnsupported; 33 | case "STATE_UNAUTHORIZED": 34 | return stateUnauthorized; 35 | } 36 | 37 | return stateUnknown; 38 | } 39 | 40 | /// The defined [String] value of the bluetooth state. 41 | final String value; 42 | 43 | /// This will `true` only if this bluetooth state suit Android system. 44 | final bool isAndroid; 45 | 46 | /// This will `true` only if this bluetooth state suit iOS system. 47 | final bool isIOS; 48 | 49 | /// Shows that bluetooth state is off. 50 | static const stateOff = BluetoothState.init( 51 | 'STATE_OFF', 52 | ); 53 | 54 | /// Shows that bluetooth state is turning off. 55 | /// 56 | /// Only for Android 57 | static const stateTurningOff = BluetoothState.init( 58 | 'STATE_TURNING_OFF', 59 | isIOS: false, 60 | ); 61 | 62 | /// Shows that bluetooth state is on. 63 | static const stateOn = BluetoothState.init( 64 | 'STATE_ON', 65 | ); 66 | 67 | /// Shows that bluetooth state is turning on. 68 | /// 69 | /// Only in Android 70 | static const stateTurningOn = BluetoothState.init( 71 | 'STATE_TURNING_ON', 72 | isIOS: false, 73 | ); 74 | 75 | /// Shows that bluetooth state is unknown. This is the default. 76 | static const stateUnknown = BluetoothState.init( 77 | 'STATE_UNKNOWN', 78 | ); 79 | 80 | /// Shows that bluetooth state is resetting. 81 | /// 82 | /// Only for iOS 83 | static const stateResetting = BluetoothState.init( 84 | 'STATE_RESETTING', 85 | isAndroid: false, 86 | ); 87 | 88 | /// Shows that bluetooth state is unsupported. 89 | static const stateUnsupported = BluetoothState.init( 90 | 'STATE_UNSUPPORTED', 91 | ); 92 | 93 | /// Shows that bluetooth state is unauthorized. 94 | /// 95 | /// Only for iOS 96 | static const stateUnauthorized = BluetoothState.init( 97 | 'STATE_UNAUTHORIZED', 98 | isAndroid: false, 99 | ); 100 | 101 | @override 102 | bool operator ==(Object other) => 103 | identical(this, other) || 104 | other is BluetoothState && 105 | runtimeType == other.runtimeType && 106 | value == other.value && 107 | isAndroid == other.isAndroid && 108 | isIOS == other.isIOS; 109 | 110 | @override 111 | int get hashCode => value.hashCode ^ isAndroid.hashCode ^ isIOS.hashCode; 112 | 113 | @override 114 | String toString() { 115 | return value; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutterbeacon/FlutterPlatform.java: -------------------------------------------------------------------------------- 1 | package com.flutterbeacon; 2 | 3 | import android.Manifest; 4 | import android.annotation.SuppressLint; 5 | import android.app.Activity; 6 | import android.bluetooth.BluetoothAdapter; 7 | import android.bluetooth.BluetoothManager; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.pm.PackageManager; 11 | import android.location.LocationManager; 12 | import android.os.Build; 13 | import android.provider.Settings; 14 | 15 | import androidx.core.app.ActivityCompat; 16 | import androidx.core.content.ContextCompat; 17 | 18 | import org.altbeacon.beacon.BeaconTransmitter; 19 | 20 | import java.lang.ref.WeakReference; 21 | 22 | class FlutterPlatform { 23 | private final WeakReference activityWeakReference; 24 | 25 | FlutterPlatform(Activity activity) { 26 | activityWeakReference = new WeakReference<>(activity); 27 | } 28 | 29 | private Activity getActivity() { 30 | return activityWeakReference.get(); 31 | } 32 | 33 | void openLocationSettings() { 34 | Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); 35 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 36 | getActivity().startActivity(intent); 37 | } 38 | 39 | void openBluetoothSettings() { 40 | Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); 41 | getActivity().startActivityForResult(intent, FlutterBeaconPlugin.REQUEST_CODE_BLUETOOTH); 42 | } 43 | 44 | void requestAuthorization() { 45 | ActivityCompat.requestPermissions(getActivity(), new String[]{ 46 | Manifest.permission.ACCESS_COARSE_LOCATION, 47 | Manifest.permission.ACCESS_FINE_LOCATION 48 | }, FlutterBeaconPlugin.REQUEST_CODE_LOCATION); 49 | } 50 | 51 | boolean checkLocationServicesPermission() { 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 53 | return ContextCompat.checkSelfPermission(getActivity(), 54 | Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; 55 | } 56 | 57 | return true; 58 | } 59 | 60 | boolean checkLocationServicesIfEnabled() { 61 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 62 | LocationManager locationManager = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE); 63 | return locationManager != null && locationManager.isLocationEnabled(); 64 | } 65 | 66 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 67 | int mode = Settings.Secure.getInt(getActivity().getContentResolver(), Settings.Secure.LOCATION_MODE, 68 | Settings.Secure.LOCATION_MODE_OFF); 69 | return (mode != Settings.Secure.LOCATION_MODE_OFF); 70 | } 71 | 72 | return true; 73 | } 74 | 75 | @SuppressLint("MissingPermission") 76 | boolean checkBluetoothIfEnabled() { 77 | BluetoothManager bluetoothManager = (BluetoothManager) 78 | getActivity().getSystemService(Context.BLUETOOTH_SERVICE); 79 | if (bluetoothManager == null) { 80 | throw new RuntimeException("No bluetooth service"); 81 | } 82 | 83 | BluetoothAdapter adapter = bluetoothManager.getAdapter(); 84 | 85 | return (adapter != null) && (adapter.isEnabled()); 86 | } 87 | 88 | boolean isBroadcastSupported() { 89 | return BeaconTransmitter.checkTransmissionSupported(getActivity()) == 0; 90 | } 91 | 92 | boolean shouldShowRequestPermissionRationale(String permission) { 93 | return ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permission); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /android/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /android/src/main/java/com/flutterbeacon/FlutterBeaconUtils.java: -------------------------------------------------------------------------------- 1 | package com.flutterbeacon; 2 | 3 | import android.util.Log; 4 | 5 | import org.altbeacon.beacon.Beacon; 6 | import org.altbeacon.beacon.Identifier; 7 | import org.altbeacon.beacon.MonitorNotifier; 8 | import org.altbeacon.beacon.Region; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Locale; 15 | import java.util.Map; 16 | 17 | class FlutterBeaconUtils { 18 | static String parseState(int state) { 19 | return state == MonitorNotifier.INSIDE ? "INSIDE" : state == MonitorNotifier.OUTSIDE ? "OUTSIDE" : "UNKNOWN"; 20 | } 21 | 22 | static List> beaconsToArray(List beacons) { 23 | if (beacons == null) { 24 | return new ArrayList<>(); 25 | } 26 | List> list = new ArrayList<>(); 27 | for (Beacon beacon : beacons) { 28 | Map map = beaconToMap(beacon); 29 | list.add(map); 30 | } 31 | 32 | return list; 33 | } 34 | 35 | private static Map beaconToMap(Beacon beacon) { 36 | Map map = new HashMap<>(); 37 | 38 | map.put("proximityUUID", beacon.getId1().toString().toUpperCase()); 39 | map.put("major", beacon.getId2().toInt()); 40 | map.put("minor", beacon.getId3().toInt()); 41 | map.put("rssi", beacon.getRssi()); 42 | map.put("txPower", beacon.getTxPower()); 43 | map.put("accuracy", String.format(Locale.US, "%.2f", beacon.getDistance())); 44 | map.put("macAddress", beacon.getBluetoothAddress()); 45 | 46 | return map; 47 | } 48 | 49 | static Map regionToMap(Region region) { 50 | Map map = new HashMap<>(); 51 | 52 | map.put("identifier", region.getUniqueId()); 53 | if (region.getId1() != null) { 54 | map.put("proximityUUID", region.getId1().toString()); 55 | } 56 | if (region.getId2() != null) { 57 | map.put("major", region.getId2().toInt()); 58 | } 59 | if (region.getId3() != null) { 60 | map.put("minor", region.getId3().toInt()); 61 | } 62 | 63 | return map; 64 | } 65 | 66 | @SuppressWarnings("rawtypes") 67 | static Region regionFromMap(Map map) { 68 | try { 69 | String identifier = ""; 70 | List identifiers = new ArrayList<>(); 71 | 72 | Object objectIdentifier = map.get("identifier"); 73 | if (objectIdentifier instanceof String) { 74 | identifier = objectIdentifier.toString(); 75 | } 76 | 77 | Object proximityUUID = map.get("proximityUUID"); 78 | 79 | if (proximityUUID instanceof String) { 80 | identifiers.add(Identifier.parse((String) proximityUUID)); 81 | } 82 | 83 | Object major = map.get("major"); 84 | if (major instanceof Integer) { 85 | identifiers.add(Identifier.fromInt((Integer) major)); 86 | } 87 | Object minor = map.get("minor"); 88 | if (minor instanceof Integer) { 89 | identifiers.add(Identifier.fromInt((Integer) minor)); 90 | } 91 | 92 | return new Region(identifier, identifiers); 93 | } catch (IllegalArgumentException e) { 94 | Log.e("REGION", "Error : " + e); 95 | return null; 96 | } 97 | } 98 | 99 | @SuppressWarnings("rawtypes") 100 | static Beacon beaconFromMap(Map map) { 101 | Beacon.Builder builder = new Beacon.Builder(); 102 | 103 | Object proximityUUID = map.get("proximityUUID"); 104 | if (proximityUUID instanceof String) { 105 | builder.setId1((String) proximityUUID); 106 | } 107 | Object major = map.get("major"); 108 | if (major instanceof Integer) { 109 | builder.setId2(major.toString()); 110 | } 111 | Object minor = map.get("minor"); 112 | if (minor instanceof Integer) { 113 | builder.setId3(minor.toString()); 114 | } 115 | 116 | Object txPower = map.get("txPower"); 117 | if (txPower instanceof Integer) { 118 | builder.setTxPower((Integer) txPower); 119 | } else { 120 | builder.setTxPower(-59); 121 | } 122 | 123 | builder.setDataFields(Collections.singletonList(0L)); 124 | builder.setManufacturer(0x004c); 125 | 126 | return builder.build(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/lib/view/app_scanning.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | import 'package:flutter_beacon/flutter_beacon.dart'; 5 | import 'package:flutter_beacon_example/controller/requirement_state_controller.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | class TabScanning extends StatefulWidget { 9 | @override 10 | _TabScanningState createState() => _TabScanningState(); 11 | } 12 | 13 | class _TabScanningState extends State { 14 | StreamSubscription? _streamRanging; 15 | final _regionBeacons = >{}; 16 | final _beacons = []; 17 | final controller = Get.find(); 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | 23 | controller.startStream.listen((flag) { 24 | if (flag == true) { 25 | initScanBeacon(); 26 | } 27 | }); 28 | 29 | controller.pauseStream.listen((flag) { 30 | if (flag == true) { 31 | pauseScanBeacon(); 32 | } 33 | }); 34 | } 35 | 36 | initScanBeacon() async { 37 | await flutterBeacon.initializeScanning; 38 | if (!controller.authorizationStatusOk || 39 | !controller.locationServiceEnabled || 40 | !controller.bluetoothEnabled) { 41 | print( 42 | 'RETURNED, authorizationStatusOk=${controller.authorizationStatusOk}, ' 43 | 'locationServiceEnabled=${controller.locationServiceEnabled}, ' 44 | 'bluetoothEnabled=${controller.bluetoothEnabled}'); 45 | return; 46 | } 47 | final regions = [ 48 | Region( 49 | identifier: 'Cubeacon', 50 | proximityUUID: 'CB10023F-A318-3394-4199-A8730C7C1AEC', 51 | ), 52 | Region( 53 | identifier: 'BeaconType2', 54 | proximityUUID: '6a84c716-0f2a-1ce9-f210-6a63bd873dd9', 55 | ), 56 | ]; 57 | 58 | if (_streamRanging != null) { 59 | if (_streamRanging!.isPaused) { 60 | _streamRanging?.resume(); 61 | return; 62 | } 63 | } 64 | 65 | _streamRanging = 66 | flutterBeacon.ranging(regions).listen((RangingResult result) { 67 | print(result); 68 | if (mounted) { 69 | setState(() { 70 | _regionBeacons[result.region] = result.beacons; 71 | _beacons.clear(); 72 | _regionBeacons.values.forEach((list) { 73 | _beacons.addAll(list); 74 | }); 75 | _beacons.sort(_compareParameters); 76 | }); 77 | } 78 | }); 79 | } 80 | 81 | pauseScanBeacon() async { 82 | _streamRanging?.pause(); 83 | if (_beacons.isNotEmpty) { 84 | setState(() { 85 | _beacons.clear(); 86 | }); 87 | } 88 | } 89 | 90 | int _compareParameters(Beacon a, Beacon b) { 91 | int compare = a.proximityUUID.compareTo(b.proximityUUID); 92 | 93 | if (compare == 0) { 94 | compare = a.major.compareTo(b.major); 95 | } 96 | 97 | if (compare == 0) { 98 | compare = a.minor.compareTo(b.minor); 99 | } 100 | 101 | return compare; 102 | } 103 | 104 | @override 105 | void dispose() { 106 | _streamRanging?.cancel(); 107 | super.dispose(); 108 | } 109 | 110 | @override 111 | Widget build(BuildContext context) { 112 | return Scaffold( 113 | body: _beacons.isEmpty 114 | ? Center(child: CircularProgressIndicator()) 115 | : ListView( 116 | children: ListTile.divideTiles( 117 | context: context, 118 | tiles: _beacons.map( 119 | (beacon) { 120 | return ListTile( 121 | title: Text( 122 | beacon.proximityUUID, 123 | style: TextStyle(fontSize: 15.0), 124 | ), 125 | subtitle: new Row( 126 | mainAxisSize: MainAxisSize.max, 127 | children: [ 128 | Flexible( 129 | child: Text( 130 | 'Major: ${beacon.major}\nMinor: ${beacon.minor}', 131 | style: TextStyle(fontSize: 13.0), 132 | ), 133 | flex: 1, 134 | fit: FlexFit.tight, 135 | ), 136 | Flexible( 137 | child: Text( 138 | 'Accuracy: ${beacon.accuracy}m\nRSSI: ${beacon.rssi}', 139 | style: TextStyle(fontSize: 13.0), 140 | ), 141 | flex: 2, 142 | fit: FlexFit.tight, 143 | ) 144 | ], 145 | ), 146 | ); 147 | }, 148 | ), 149 | ).toList(), 150 | ), 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/beacon/beacon_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_beacon/flutter_beacon.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | main() { 7 | test('main constructor must be equal', () { 8 | const beacon = const Beacon( 9 | proximityUUID: 'UUID', 10 | macAddress: 'MAC-ADDRESS', 11 | major: 1, 12 | minor: 2, 13 | rssi: -60, 14 | txPower: -59, 15 | accuracy: 0.0, 16 | ); 17 | 18 | expect(beacon.proximityUUID, 'UUID'); 19 | expect(beacon.macAddress, 'MAC-ADDRESS'); 20 | expect(beacon.major, 1); 21 | expect(beacon.minor, 2); 22 | expect(beacon.rssi, -60); 23 | expect(beacon.txPower, -59); 24 | expect(beacon.accuracy, 0.0); 25 | expect(beacon.proximity, Proximity.unknown); 26 | 27 | const beacon2 = const Beacon( 28 | accuracy: 0.4, 29 | proximityUUID: 'UUID', 30 | major: 1, 31 | minor: 2, 32 | txPower: -59, 33 | ); 34 | expect(beacon2.proximity, Proximity.immediate); 35 | 36 | const beacon3 = const Beacon( 37 | accuracy: 2.9, 38 | proximityUUID: 'UUID', 39 | major: 1, 40 | minor: 2, 41 | txPower: -59, 42 | rssi: null, 43 | ); 44 | expect(beacon3.proximity, Proximity.near); 45 | expect(beacon3.rssi, -1); 46 | }); 47 | 48 | test('constructor from json must be equal', () { 49 | final beacon = Beacon.fromJson({ 50 | 'proximityUUID': 'UUID', 51 | 'macAddress': 'MAC-ADDRESS', 52 | 'major': 1, 53 | 'minor': 2, 54 | 'rssi': '-60', 55 | 'txPower': '-59', 56 | 'accuracy': '1.23', 57 | 'proximity': 'far', 58 | }); 59 | 60 | expect(beacon.proximityUUID, 'UUID'); 61 | expect(beacon.macAddress, 'MAC-ADDRESS'); 62 | expect(beacon.major, 1); 63 | expect(beacon.minor, 2); 64 | expect(beacon.rssi, -60); 65 | expect(beacon.txPower, -59); 66 | expect(beacon.accuracy, 1.23); 67 | }); 68 | 69 | test('beacon must be equal', () { 70 | final beacon1 = Beacon.fromJson({ 71 | 'proximityUUID': 'UUID', 72 | 'macAddress': 'MAC-ADDRESS', 73 | 'major': 1, 74 | 'minor': 2, 75 | 'rssi': '-60', 76 | 'txPower': '-59', 77 | 'accuracy': '1.23', 78 | 'proximity': 'far', 79 | }); 80 | final beacon2 = Beacon.fromJson({ 81 | 'proximityUUID': 'UUID', 82 | 'macAddress': 'MAC-ADDRESS', 83 | 'major': 1, 84 | 'minor': 2, 85 | 'rssi': '-60', 86 | 'txPower': '-59', 87 | 'accuracy': '1.23', 88 | 'proximity': 'far', 89 | }); 90 | 91 | expect(beacon1 == beacon2, isTrue); 92 | expect(beacon1.hashCode == beacon2.hashCode, isTrue); 93 | }); 94 | 95 | test('parsing beacon array must be empty', () { 96 | expect(Beacon.beaconFromArray(Object()).isEmpty, isTrue); 97 | }); 98 | 99 | test('parsing beacon array length must be equal to "2"', () { 100 | final beacons = [ 101 | Beacon.fromJson({ 102 | 'proximityUUID': 'UUID', 103 | 'macAddress': 'MAC-ADDRESS', 104 | 'major': 1, 105 | 'minor': 2, 106 | 'rssi': '-60', 107 | 'txPower': '-59', 108 | 'accuracy': '1.23', 109 | 'proximity': 'far', 110 | }), 111 | Beacon.fromJson({ 112 | 'proximityUUID': 'UUID', 113 | 'macAddress': 'MAC-ADDRESS', 114 | 'major': 1, 115 | 'minor': 2, 116 | 'rssi': '-60', 117 | 'txPower': '-59', 118 | 'accuracy': '1.23', 119 | 'proximity': 'far', 120 | }), 121 | ]; 122 | expect(Beacon.beaconArrayToJson(beacons) is List, isTrue); 123 | expect( 124 | Beacon.beaconFromArray(Beacon.beaconArrayToJson(beacons)) 125 | is List, 126 | isTrue); 127 | }); 128 | 129 | test('beacon json must be equal', () { 130 | final beacon = Beacon.fromJson({ 131 | 'macAddress': 'MAC', 132 | 'proximityUUID': 'UUID', 133 | 'major': 1, 134 | 'minor': 2, 135 | 'rssi': -60, 136 | 'accuracy': 1.23, 137 | 'proximity': 'far', 138 | }); 139 | final beacon2 = Beacon.fromJson({ 140 | 'macAddress': 'MAC', 141 | 'proximityUUID': 'UUID', 142 | 'major': 1, 143 | 'minor': 2, 144 | 'rssi': -60, 145 | 'accuracy': 1.23, 146 | 'proximity': 'far', 147 | }); 148 | 149 | expect(beacon, beacon2); 150 | expect(beacon.hashCode, beacon2.hashCode); 151 | expect(beacon.toJson, { 152 | 'macAddress': 'MAC', 153 | 'proximityUUID': 'UUID', 154 | 'major': 1, 155 | 'minor': 2, 156 | 'rssi': -60, 157 | 'accuracy': 1.23, 158 | 'proximity': 'far', 159 | }); 160 | expect( 161 | beacon.toString(), 162 | json.encode({ 163 | 'proximityUUID': 'UUID', 164 | 'major': 1, 165 | 'minor': 2, 166 | 'rssi': -60, 167 | 'accuracy': 1.23, 168 | 'proximity': 'far', 169 | 'macAddress': 'MAC', 170 | })); 171 | }); 172 | } 173 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/dart,flutter,androidstudio,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=dart,flutter,androidstudio,visualstudiocode 4 | 5 | ### Dart ### 6 | # See https://www.dartlang.org/guides/libraries/private-files 7 | 8 | # Files and directories created by pub 9 | .dart_tool/ 10 | .packages 11 | build/ 12 | # If you're building an application, you may want to check-in your pubspec.lock 13 | pubspec.lock 14 | 15 | # Directory created by dartdoc 16 | # If you don't generate documentation locally you can remove this line. 17 | doc/api/ 18 | 19 | # Avoid committing generated Javascript files: 20 | *.dart.js 21 | *.info.json # Produced by the --dump-info flag. 22 | *.js # When generated by dart2js. Don't specify *.js if your 23 | # project includes source files written in JavaScript. 24 | *.js_ 25 | *.js.deps 26 | *.js.map 27 | 28 | ### Flutter ### 29 | # Flutter/Dart/Pub related 30 | **/doc/api/ 31 | .flutter-plugins 32 | .pub-cache/ 33 | .pub/ 34 | .flutter-plugins-dependencies 35 | flutter_export_environment.sh 36 | 37 | # Android related 38 | **/android/**/gradle-wrapper.jar 39 | **/android/.gradle 40 | **/android/captures/ 41 | **/android/gradlew 42 | **/android/gradlew.bat 43 | **/android/local.properties 44 | **/android/**/GeneratedPluginRegistrant.java 45 | 46 | # iOS/XCode related 47 | **/ios/**/*.mode1v3 48 | **/ios/**/*.mode2v3 49 | **/ios/**/*.moved-aside 50 | **/ios/**/*.pbxuser 51 | **/ios/**/*.perspectivev3 52 | **/ios/**/*sync/ 53 | **/ios/**/.sconsign.dblite 54 | **/ios/**/.tags* 55 | **/ios/**/.vagrant/ 56 | **/ios/**/DerivedData/ 57 | **/ios/**/Icon? 58 | **/ios/**/Pods/ 59 | **/ios/**/.symlinks/ 60 | **/ios/**/profile 61 | **/ios/**/xcuserdata 62 | **/ios/.generated/ 63 | **/ios/Flutter/App.framework 64 | **/ios/Flutter/Flutter.framework 65 | **/ios/Flutter/Generated.xcconfig 66 | **/ios/Flutter/app.flx 67 | **/ios/Flutter/app.zip 68 | **/ios/Flutter/flutter_assets/ 69 | **/ios/ServiceDefinitions.json 70 | **/ios/Runner/GeneratedPluginRegistrant.* 71 | 72 | # Exceptions to above rules. 73 | !**/ios/**/default.mode1v3 74 | !**/ios/**/default.mode2v3 75 | !**/ios/**/default.pbxuser 76 | !**/ios/**/default.perspectivev3 77 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 78 | 79 | ### VisualStudioCode ### 80 | .vscode/* 81 | !.vscode/settings.json 82 | !.vscode/tasks.json 83 | !.vscode/launch.json 84 | !.vscode/extensions.json 85 | 86 | ### VisualStudioCode Patch ### 87 | # Ignore all local history of files 88 | .history 89 | 90 | ### AndroidStudio ### 91 | # Covers files to be ignored for android development using Android Studio. 92 | 93 | # Built application files 94 | *.apk 95 | *.ap_ 96 | 97 | # Files for the ART/Dalvik VM 98 | *.dex 99 | 100 | # Java class files 101 | *.class 102 | 103 | # Generated files 104 | bin/ 105 | gen/ 106 | out/ 107 | 108 | # Gradle files 109 | .gradle 110 | .gradle/ 111 | 112 | # Signing files 113 | .signing/ 114 | 115 | # Local configuration file (sdk path, etc) 116 | local.properties 117 | 118 | # Proguard folder generated by Eclipse 119 | proguard/ 120 | 121 | # Log Files 122 | *.log 123 | 124 | # Android Studio 125 | /*/build/ 126 | /*/local.properties 127 | /*/out 128 | /*/*/build 129 | /*/*/production 130 | captures/ 131 | .navigation/ 132 | *.ipr 133 | *~ 134 | *.swp 135 | 136 | # Android Patch 137 | gen-external-apklibs 138 | 139 | # External native build folder generated in Android Studio 2.2 and later 140 | .externalNativeBuild 141 | 142 | # NDK 143 | obj/ 144 | 145 | # IntelliJ IDEA 146 | *.iml 147 | *.iws 148 | /out/ 149 | 150 | # User-specific configurations 151 | .idea/caches/ 152 | .idea/libraries/ 153 | .idea/shelf/ 154 | .idea/workspace.xml 155 | .idea/tasks.xml 156 | .idea/.name 157 | .idea/compiler.xml 158 | .idea/copyright/profiles_settings.xml 159 | .idea/encodings.xml 160 | .idea/misc.xml 161 | .idea/modules.xml 162 | .idea/scopes/scope_settings.xml 163 | .idea/dictionaries 164 | .idea/vcs.xml 165 | .idea/jsLibraryMappings.xml 166 | .idea/datasources.xml 167 | .idea/dataSources.ids 168 | .idea/sqlDataSources.xml 169 | .idea/dynamic.xml 170 | .idea/uiDesigner.xml 171 | .idea/assetWizardSettings.xml 172 | 173 | # OS-specific files 174 | .DS_Store 175 | .DS_Store? 176 | ._* 177 | .Spotlight-V100 178 | .Trashes 179 | ehthumbs.db 180 | Thumbs.db 181 | 182 | # Legacy Eclipse project files 183 | .classpath 184 | .project 185 | .cproject 186 | .settings/ 187 | 188 | # Mobile Tools for Java (J2ME) 189 | .mtj.tmp/ 190 | 191 | # Package Files # 192 | *.war 193 | *.ear 194 | 195 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 196 | hs_err_pid* 197 | 198 | ## Plugin-specific files: 199 | 200 | # mpeltonen/sbt-idea plugin 201 | .idea_modules/ 202 | 203 | # JIRA plugin 204 | atlassian-ide-plugin.xml 205 | 206 | # Mongo Explorer plugin 207 | .idea/mongoSettings.xml 208 | 209 | # Crashlytics plugin (for Android Studio and IntelliJ) 210 | com_crashlytics_export_strings.xml 211 | crashlytics.properties 212 | crashlytics-build.properties 213 | fabric.properties 214 | 215 | ### AndroidStudio Patch ### 216 | 217 | !/gradle/wrapper/gradle-wrapper.jar 218 | 219 | # Test, Coverage 220 | coverage 221 | test/.test_coverage.dart 222 | 223 | example/ios/Flutter/.last_build_id 224 | 225 | # End of https://www.gitignore.io/api/dart,flutter,androidstudio,visualstudiocode -------------------------------------------------------------------------------- /lib/beacon/beacon.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | part of flutter_beacon; 6 | 7 | /// Enum for defining proximity. 8 | enum Proximity { unknown, immediate, near, far } 9 | 10 | /// Class for managing Beacon object. 11 | class Beacon { 12 | /// The proximity UUID of beacon. 13 | final String proximityUUID; 14 | 15 | /// The mac address of beacon. 16 | /// 17 | /// From iOS this value will be null 18 | final String? macAddress; 19 | 20 | /// The major value of beacon. 21 | final int major; 22 | 23 | /// The minor value of beacon. 24 | final int minor; 25 | 26 | /// The rssi value of beacon. 27 | final int rssi; 28 | 29 | /// The transmission power of beacon. 30 | /// 31 | /// From iOS this value will be null 32 | final int? txPower; 33 | 34 | /// The accuracy of distance of beacon in meter. 35 | final double accuracy; 36 | 37 | /// The proximity of beacon. 38 | final Proximity? _proximity; 39 | 40 | /// Create beacon object. 41 | const Beacon({ 42 | required this.proximityUUID, 43 | this.macAddress, 44 | required this.major, 45 | required this.minor, 46 | int? rssi, 47 | this.txPower, 48 | required this.accuracy, 49 | Proximity? proximity, 50 | }) : this.rssi = rssi ?? -1, 51 | this._proximity = proximity; 52 | 53 | /// Create beacon object from json. 54 | Beacon.fromJson(dynamic json) 55 | : this( 56 | proximityUUID: json['proximityUUID'], 57 | macAddress: json['macAddress'], 58 | major: json['major'], 59 | minor: json['minor'], 60 | rssi: _parseInt(json['rssi']), 61 | txPower: _parseInt(json['txPower']), 62 | accuracy: _parseDouble(json['accuracy']), 63 | proximity: _parseProximity(json['proximity']), 64 | ); 65 | 66 | /// Parsing dynamic data into double. 67 | static double _parseDouble(dynamic data) { 68 | if (data is num) { 69 | return data.toDouble(); 70 | } else if (data is String) { 71 | return double.tryParse(data) ?? 0.0; 72 | } 73 | 74 | return 0.0; 75 | } 76 | 77 | /// Parsing dynamic data into integer. 78 | static int? _parseInt(dynamic data) { 79 | if (data is num) { 80 | return data.toInt(); 81 | } else if (data is String) { 82 | return int.tryParse(data) ?? 0; 83 | } 84 | 85 | return null; 86 | } 87 | 88 | /// Parsing dynamic proximity into enum [Proximity]. 89 | static dynamic _parseProximity(dynamic proximity) { 90 | if (proximity == 'unknown') { 91 | return Proximity.unknown; 92 | } 93 | 94 | if (proximity == 'immediate') { 95 | return Proximity.immediate; 96 | } 97 | 98 | if (proximity == 'near') { 99 | return Proximity.near; 100 | } 101 | 102 | if (proximity == 'far') { 103 | return Proximity.far; 104 | } 105 | 106 | return null; 107 | } 108 | 109 | /// Parsing array of [Map] into [List] of [Beacon]. 110 | static List beaconFromArray(dynamic beacons) { 111 | if (beacons is List) { 112 | return beacons.map((json) { 113 | return Beacon.fromJson(json); 114 | }).toList(); 115 | } 116 | 117 | return []; 118 | } 119 | 120 | /// Parsing [List] of [Beacon] into array of [Map]. 121 | static dynamic beaconArrayToJson(List beacons) { 122 | return beacons.map((beacon) { 123 | return beacon.toJson; 124 | }).toList(); 125 | } 126 | 127 | /// Serialize current instance object into [Map]. 128 | dynamic get toJson { 129 | final map = { 130 | 'proximityUUID': proximityUUID, 131 | 'major': major, 132 | 'minor': minor, 133 | 'rssi': rssi, 134 | 'accuracy': accuracy, 135 | 'proximity': proximity.toString().split('.').last 136 | }; 137 | 138 | if (txPower != null) { 139 | map['txPower'] = txPower; 140 | } 141 | 142 | if (macAddress != null) { 143 | map['macAddress'] = macAddress; 144 | } 145 | 146 | return map; 147 | } 148 | 149 | /// Return [Proximity] of beacon. 150 | /// 151 | /// iOS will always set proximity by default, but Android is not 152 | /// so we manage it by filtering the accuracy like bellow : 153 | /// - `accuracy == 0.0` : [Proximity.unknown] 154 | /// - `accuracy > 0 && accuracy <= 0.5` : [Proximity.immediate] 155 | /// - `accuracy > 0.5 && accuracy < 3.0` : [Proximity.near] 156 | /// - `accuracy > 3.0` : [Proximity.far] 157 | Proximity get proximity { 158 | if (_proximity != null) { 159 | return _proximity!; 160 | } 161 | 162 | if (accuracy == 0.0) { 163 | return Proximity.unknown; 164 | } 165 | 166 | if (accuracy <= 0.5) { 167 | return Proximity.immediate; 168 | } 169 | 170 | if (accuracy < 3.0) { 171 | return Proximity.near; 172 | } 173 | 174 | return Proximity.far; 175 | } 176 | 177 | @override 178 | bool operator ==(Object other) => 179 | identical(this, other) || 180 | other is Beacon && 181 | runtimeType == other.runtimeType && 182 | proximityUUID == other.proximityUUID && 183 | major == other.major && 184 | minor == other.minor && 185 | (macAddress != null ? macAddress == other.macAddress : true); 186 | 187 | @override 188 | int get hashCode { 189 | int hashCode = proximityUUID.hashCode ^ major.hashCode ^ minor.hashCode; 190 | if (macAddress != null) { 191 | hashCode = hashCode ^ macAddress.hashCode; 192 | } 193 | 194 | return hashCode; 195 | } 196 | 197 | @override 198 | String toString() { 199 | return json.encode(toJson); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /example/lib/view/app_broadcasting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_beacon/flutter_beacon.dart'; 5 | import 'package:flutter_beacon_example/controller/requirement_state_controller.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | class TabBroadcasting extends StatefulWidget { 9 | @override 10 | _TabBroadcastingState createState() => _TabBroadcastingState(); 11 | } 12 | 13 | class _TabBroadcastingState extends State { 14 | final controller = Get.find(); 15 | final clearFocus = FocusNode(); 16 | bool broadcasting = false; 17 | 18 | final regexUUID = RegExp( 19 | r'[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}'); 20 | final uuidController = 21 | TextEditingController(text: 'CB10023F-A318-3394-4199-A8730C7C1AEC'); 22 | final majorController = TextEditingController(text: '0'); 23 | final minorController = TextEditingController(text: '0'); 24 | 25 | bool get broadcastReady => 26 | controller.authorizationStatusOk == true && 27 | controller.locationServiceEnabled == true && 28 | controller.bluetoothEnabled == true; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | 34 | controller.startBroadcastStream.listen((flag) { 35 | if (flag == true) { 36 | initBroadcastBeacon(); 37 | } 38 | }); 39 | } 40 | 41 | initBroadcastBeacon() async { 42 | await flutterBeacon.initializeScanning; 43 | } 44 | 45 | @override 46 | void dispose() { 47 | clearFocus.dispose(); 48 | super.dispose(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | body: GestureDetector( 55 | onTap: () => FocusScope.of(context).requestFocus(clearFocus), 56 | child: Obx( 57 | () => broadcastReady != true 58 | ? Center(child: Text('Please wait...')) 59 | : Form( 60 | autovalidateMode: AutovalidateMode.onUserInteraction, 61 | child: Container( 62 | padding: const EdgeInsets.symmetric( 63 | horizontal: 16, 64 | vertical: 8, 65 | ), 66 | child: Column( 67 | crossAxisAlignment: CrossAxisAlignment.stretch, 68 | children: [ 69 | uuidField, 70 | majorField, 71 | minorField, 72 | SizedBox(height: 16), 73 | buttonBroadcast, 74 | ], 75 | ), 76 | ), 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | 83 | Widget get uuidField { 84 | return TextFormField( 85 | readOnly: broadcasting, 86 | controller: uuidController, 87 | decoration: InputDecoration( 88 | labelText: 'Proximity UUID', 89 | ), 90 | validator: (val) { 91 | if (val == null || val.isEmpty) { 92 | return 'Proximity UUID required'; 93 | } 94 | 95 | if (!regexUUID.hasMatch(val)) { 96 | return 'Invalid Proxmity UUID format'; 97 | } 98 | 99 | return null; 100 | }, 101 | ); 102 | } 103 | 104 | Widget get majorField { 105 | return TextFormField( 106 | readOnly: broadcasting, 107 | controller: majorController, 108 | decoration: InputDecoration( 109 | labelText: 'Major', 110 | ), 111 | keyboardType: TextInputType.number, 112 | validator: (val) { 113 | if (val == null || val.isEmpty) { 114 | return 'Major required'; 115 | } 116 | 117 | try { 118 | int major = int.parse(val); 119 | 120 | if (major < 0 || major > 65535) { 121 | return 'Major must be number between 0 and 65535'; 122 | } 123 | } on FormatException { 124 | return 'Major must be number'; 125 | } 126 | 127 | return null; 128 | }, 129 | ); 130 | } 131 | 132 | Widget get minorField { 133 | return TextFormField( 134 | readOnly: broadcasting, 135 | controller: minorController, 136 | decoration: InputDecoration( 137 | labelText: 'Minor', 138 | ), 139 | keyboardType: TextInputType.number, 140 | validator: (val) { 141 | if (val == null || val.isEmpty) { 142 | return 'Minor required'; 143 | } 144 | 145 | try { 146 | int minor = int.parse(val); 147 | 148 | if (minor < 0 || minor > 65535) { 149 | return 'Minor must be number between 0 and 65535'; 150 | } 151 | } on FormatException { 152 | return 'Minor must be number'; 153 | } 154 | 155 | return null; 156 | }, 157 | ); 158 | } 159 | 160 | Widget get buttonBroadcast { 161 | final ButtonStyle raisedButtonStyle = ElevatedButton.styleFrom( 162 | onPrimary: Colors.white, 163 | primary: broadcasting ? Colors.red : Theme.of(context).primaryColor, 164 | minimumSize: Size(88, 36), 165 | padding: EdgeInsets.symmetric(horizontal: 16), 166 | shape: const RoundedRectangleBorder( 167 | borderRadius: BorderRadius.all(Radius.circular(2)), 168 | ), 169 | ); 170 | 171 | return ElevatedButton( 172 | style: raisedButtonStyle, 173 | onPressed: () async { 174 | if (broadcasting) { 175 | await flutterBeacon.stopBroadcast(); 176 | } else { 177 | await flutterBeacon.startBroadcast(BeaconBroadcast( 178 | proximityUUID: uuidController.text, 179 | major: int.tryParse(majorController.text) ?? 0, 180 | minor: int.tryParse(minorController.text) ?? 0, 181 | )); 182 | } 183 | 184 | final isBroadcasting = await flutterBeacon.isBroadcasting(); 185 | 186 | if (mounted) { 187 | setState(() { 188 | broadcasting = isBroadcasting; 189 | }); 190 | } 191 | }, 192 | child: Text('Broadcast${broadcasting ? 'ing' : ''}'), 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Beacon 2 | 3 | [![Pub](https://img.shields.io/pub/v/flutter_beacon.svg)](https://pub.dartlang.org/packages/flutter_beacon) [![GitHub](https://img.shields.io/github/license/alann-maulana/flutter_beacon.svg?color=2196F3)](https://github.com/alann-maulana/flutter_beacon/blob/master/LICENSE) [![Build](https://github.com/alann-maulana/flutter_beacon/workflows/Flutter%20CI/badge.svg)](https://github.com/alann-maulana/flutter_beacon/actions?query=workflow%3A%22Flutter+CI%22) [![Coverage Status](https://coveralls.io/repos/github/alann-maulana/flutter_beacon/badge.svg?branch=master)](https://coveralls.io/github/alann-maulana/flutter_beacon?branch=master) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Falann-maulana%2Fflutter_beacon.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Falann-maulana%2Fflutter_beacon?ref=badge_shield) [![codecov](https://codecov.io/gh/alann-maulana/flutter_beacon/branch/master/graph/badge.svg)](https://codecov.io/gh/alann-maulana/flutter_beacon) 4 | 5 | [Flutter plugin](https://pub.dartlang.org/packages/flutter_beacon/) to work with iBeacons. 6 | 7 | An hybrid iBeacon scanner and transmitter SDK for Flutter plugin. Supports Android API 18+ and iOS 8+. 8 | 9 | Features: 10 | 11 | * Automatic permission management 12 | * Ranging iBeacons 13 | * Monitoring iBeacons 14 | * Transmit as iBeacon 15 | 16 | ## Installation 17 | 18 | Add to pubspec.yaml: 19 | 20 | ```yaml 21 | dependencies: 22 | flutter_beacon: latest 23 | ``` 24 | 25 | ### Setup specific for Android 26 | 27 | For target SDK version 29+ (Android 10, 11) is necessary to add manually ```ACCESS_FINE_LOCATION``` 28 | 29 | ``` 30 | 31 | ``` 32 | 33 | and if you want also background scanning: 34 | ``` 35 | 36 | ``` 37 | 38 | ### Setup specific for iOS 39 | 40 | In order to use beacons related features, apps are required to ask the location permission. It's a two step process: 41 | 42 | 1. Declare the permission the app requires in configuration files 43 | 2. Request the permission to the user when app is running (the plugin can handle this automatically) 44 | 45 | The needed permissions in iOS is `when in use`. 46 | 47 | For more details about what you can do with each permission, see: 48 | https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services 49 | 50 | Permission must be declared in `ios/Runner/Info.plist`: 51 | 52 | ```xml 53 | 54 | 55 | NSLocationWhenInUseUsageDescription 56 | Reason why app needs location 57 | 58 | 59 | NSLocationAlwaysAndWhenInUseUsageDescription 60 | Reason why app needs location 61 | 62 | NSLocationAlwaysUsageDescription 63 | Reason why app needs location 64 | 65 | 66 | NSBluetoothAlwaysUsageDescription 67 | Reason why app needs bluetooth 68 | 69 | ``` 70 | 71 | ## iOS Troubleshooting 72 | 73 | * Example code works properly only on **physical device** (bluetooth on simulator is disabled) 74 | * How to deploy flutter app on iOS device [Instruction](https://flutter.dev/docs/get-started/install/macos) 75 | * If example code don't works on device (beacons not appear), please make sure that you have enabled
Location and Bluetooth (Settings -> Flutter Beacon) 76 | 77 | ## How-to 78 | 79 | Ranging APIs are designed as reactive streams. 80 | 81 | * The first subscription to the stream will start the ranging 82 | 83 | ### Initializing Library 84 | 85 | ```dart 86 | try { 87 | // if you want to manage manual checking about the required permissions 88 | await flutterBeacon.initializeScanning; 89 | 90 | // or if you want to include automatic checking permission 91 | await flutterBeacon.initializeAndCheckScanning; 92 | } on PlatformException catch(e) { 93 | // library failed to initialize, check code and message 94 | } 95 | ``` 96 | 97 | ### Ranging beacons 98 | 99 | ```dart 100 | final regions = []; 101 | 102 | if (Platform.isIOS) { 103 | // iOS platform, at least set identifier and proximityUUID for region scanning 104 | regions.add(Region( 105 | identifier: 'Apple Airlocate', 106 | proximityUUID: 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0')); 107 | } else { 108 | // android platform, it can ranging out of beacon that filter all of Proximity UUID 109 | regions.add(Region(identifier: 'com.beacon')); 110 | } 111 | 112 | // to start ranging beacons 113 | _streamRanging = flutterBeacon.ranging(regions).listen((RangingResult result) { 114 | // result contains a region and list of beacons found 115 | // list can be empty if no matching beacons were found in range 116 | }); 117 | 118 | // to stop ranging beacons 119 | _streamRanging.cancel(); 120 | ``` 121 | 122 | ### Monitoring beacons 123 | 124 | ```dart 125 | final regions = []; 126 | 127 | if (Platform.isIOS) { 128 | // iOS platform, at least set identifier and proximityUUID for region scanning 129 | regions.add(Region( 130 | identifier: 'Apple Airlocate', 131 | proximityUUID: 'E2C56DB5-DFFB-48D2-B060-D0F5A71096E0')); 132 | } else { 133 | // Android platform, it can ranging out of beacon that filter all of Proximity UUID 134 | regions.add(Region(identifier: 'com.beacon')); 135 | } 136 | 137 | // to start monitoring beacons 138 | _streamMonitoring = flutterBeacon.monitoring(regions).listen((MonitoringResult result) { 139 | // result contains a region, event type and event state 140 | }); 141 | 142 | // to stop monitoring beacons 143 | _streamMonitoring.cancel(); 144 | ``` 145 | 146 | ## Under the hood 147 | 148 | * iOS uses native Framework [CoreLocation](https://developer.apple.com/documentation/corelocation/) 149 | * Android uses the [Android-Beacon-Library](https://github.com/AltBeacon/android-beacon-library) ([Apache License 2.0](https://github.com/AltBeacon/android-beacon-library/blob/master/LICENSE)) 150 | 151 | # Author 152 | 153 | Flutter Beacon plugin is developed by Eyro Labs. You can contact me at . 154 | -------------------------------------------------------------------------------- /example/lib/view/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_beacon/flutter_beacon.dart'; 7 | import 'package:flutter_beacon_example/controller/requirement_state_controller.dart'; 8 | import 'package:flutter_beacon_example/view/app_broadcasting.dart'; 9 | import 'package:flutter_beacon_example/view/app_scanning.dart'; 10 | import 'package:get/get.dart'; 11 | 12 | class HomePage extends StatefulWidget { 13 | @override 14 | _HomePageState createState() => _HomePageState(); 15 | } 16 | 17 | class _HomePageState extends State with WidgetsBindingObserver { 18 | final controller = Get.find(); 19 | StreamSubscription? _streamBluetooth; 20 | int currentIndex = 0; 21 | 22 | @override 23 | void initState() { 24 | WidgetsBinding.instance?.addObserver(this); 25 | 26 | super.initState(); 27 | 28 | listeningState(); 29 | } 30 | 31 | listeningState() async { 32 | print('Listening to bluetooth state'); 33 | _streamBluetooth = flutterBeacon 34 | .bluetoothStateChanged() 35 | .listen((BluetoothState state) async { 36 | controller.updateBluetoothState(state); 37 | await checkAllRequirements(); 38 | }); 39 | } 40 | 41 | checkAllRequirements() async { 42 | final bluetoothState = await flutterBeacon.bluetoothState; 43 | controller.updateBluetoothState(bluetoothState); 44 | print('BLUETOOTH $bluetoothState'); 45 | 46 | final authorizationStatus = await flutterBeacon.authorizationStatus; 47 | controller.updateAuthorizationStatus(authorizationStatus); 48 | print('AUTHORIZATION $authorizationStatus'); 49 | 50 | final locationServiceEnabled = 51 | await flutterBeacon.checkLocationServicesIfEnabled; 52 | controller.updateLocationService(locationServiceEnabled); 53 | print('LOCATION SERVICE $locationServiceEnabled'); 54 | 55 | if (controller.bluetoothEnabled && 56 | controller.authorizationStatusOk && 57 | controller.locationServiceEnabled) { 58 | print('STATE READY'); 59 | if (currentIndex == 0) { 60 | print('SCANNING'); 61 | controller.startScanning(); 62 | } else { 63 | print('BROADCASTING'); 64 | controller.startBroadcasting(); 65 | } 66 | } else { 67 | print('STATE NOT READY'); 68 | controller.pauseScanning(); 69 | } 70 | } 71 | 72 | @override 73 | void didChangeAppLifecycleState(AppLifecycleState state) async { 74 | print('AppLifecycleState = $state'); 75 | if (state == AppLifecycleState.resumed) { 76 | if (_streamBluetooth != null) { 77 | if (_streamBluetooth!.isPaused) { 78 | _streamBluetooth?.resume(); 79 | } 80 | } 81 | await checkAllRequirements(); 82 | } else if (state == AppLifecycleState.paused) { 83 | _streamBluetooth?.pause(); 84 | } 85 | } 86 | 87 | @override 88 | void dispose() { 89 | _streamBluetooth?.cancel(); 90 | 91 | super.dispose(); 92 | } 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | return Scaffold( 97 | appBar: AppBar( 98 | title: const Text('Flutter Beacon'), 99 | centerTitle: false, 100 | actions: [ 101 | Obx(() { 102 | if (!controller.locationServiceEnabled) 103 | return IconButton( 104 | tooltip: 'Not Determined', 105 | icon: Icon(Icons.portable_wifi_off), 106 | color: Colors.grey, 107 | onPressed: () {}, 108 | ); 109 | 110 | if (!controller.authorizationStatusOk) 111 | return IconButton( 112 | tooltip: 'Not Authorized', 113 | icon: Icon(Icons.portable_wifi_off), 114 | color: Colors.red, 115 | onPressed: () async { 116 | await flutterBeacon.requestAuthorization; 117 | }, 118 | ); 119 | 120 | return IconButton( 121 | tooltip: 'Authorized', 122 | icon: Icon(Icons.wifi_tethering), 123 | color: Colors.blue, 124 | onPressed: () async { 125 | await flutterBeacon.requestAuthorization; 126 | }, 127 | ); 128 | }), 129 | Obx(() { 130 | return IconButton( 131 | tooltip: controller.locationServiceEnabled 132 | ? 'Location Service ON' 133 | : 'Location Service OFF', 134 | icon: Icon( 135 | controller.locationServiceEnabled 136 | ? Icons.location_on 137 | : Icons.location_off, 138 | ), 139 | color: 140 | controller.locationServiceEnabled ? Colors.blue : Colors.red, 141 | onPressed: controller.locationServiceEnabled 142 | ? () {} 143 | : handleOpenLocationSettings, 144 | ); 145 | }), 146 | Obx(() { 147 | final state = controller.bluetoothState.value; 148 | 149 | if (state == BluetoothState.stateOn) { 150 | return IconButton( 151 | tooltip: 'Bluetooth ON', 152 | icon: Icon(Icons.bluetooth_connected), 153 | onPressed: () {}, 154 | color: Colors.lightBlueAccent, 155 | ); 156 | } 157 | 158 | if (state == BluetoothState.stateOff) { 159 | return IconButton( 160 | tooltip: 'Bluetooth OFF', 161 | icon: Icon(Icons.bluetooth), 162 | onPressed: handleOpenBluetooth, 163 | color: Colors.red, 164 | ); 165 | } 166 | 167 | return IconButton( 168 | icon: Icon(Icons.bluetooth_disabled), 169 | tooltip: 'Bluetooth State Unknown', 170 | onPressed: () {}, 171 | color: Colors.grey, 172 | ); 173 | }), 174 | ], 175 | ), 176 | body: IndexedStack( 177 | index: currentIndex, 178 | children: [ 179 | TabScanning(), 180 | TabBroadcasting(), 181 | ], 182 | ), 183 | bottomNavigationBar: BottomNavigationBar( 184 | currentIndex: currentIndex, 185 | onTap: (index) { 186 | setState(() { 187 | currentIndex = index; 188 | }); 189 | 190 | if (currentIndex == 0) { 191 | controller.startScanning(); 192 | } else { 193 | controller.pauseScanning(); 194 | controller.startBroadcasting(); 195 | } 196 | }, 197 | items: [ 198 | BottomNavigationBarItem( 199 | icon: Icon(Icons.list), 200 | label: 'Scan', 201 | ), 202 | BottomNavigationBarItem( 203 | icon: Icon(Icons.bluetooth_audio), 204 | label: 'Broadcast', 205 | ), 206 | ], 207 | ), 208 | ); 209 | } 210 | 211 | handleOpenLocationSettings() async { 212 | if (Platform.isAndroid) { 213 | await flutterBeacon.openLocationSettings; 214 | } else if (Platform.isIOS) { 215 | await showDialog( 216 | context: context, 217 | builder: (context) { 218 | return AlertDialog( 219 | title: Text('Location Services Off'), 220 | content: Text( 221 | 'Please enable Location Services on Settings > Privacy > Location Services.', 222 | ), 223 | actions: [ 224 | TextButton( 225 | onPressed: () => Navigator.pop(context), 226 | child: Text('OK'), 227 | ), 228 | ], 229 | ); 230 | }, 231 | ); 232 | } 233 | } 234 | 235 | handleOpenBluetooth() async { 236 | if (Platform.isAndroid) { 237 | try { 238 | await flutterBeacon.openBluetoothSettings; 239 | } on PlatformException catch (e) { 240 | print(e); 241 | } 242 | } else if (Platform.isIOS) { 243 | await showDialog( 244 | context: context, 245 | builder: (context) { 246 | return AlertDialog( 247 | title: Text('Bluetooth is Off'), 248 | content: Text('Please enable Bluetooth on Settings > Bluetooth.'), 249 | actions: [ 250 | TextButton( 251 | onPressed: () => Navigator.pop(context), 252 | child: Text('OK'), 253 | ), 254 | ], 255 | ); 256 | }, 257 | ); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lib/flutter_beacon.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Eyro Labs. 2 | // Licensed under Apache License v2.0 that can be 3 | // found in the LICENSE file. 4 | 5 | /// Flutter beacon library. 6 | library flutter_beacon; 7 | 8 | import 'dart:async'; 9 | import 'dart:convert'; 10 | import 'dart:io'; 11 | 12 | import 'package:flutter/foundation.dart'; 13 | import 'package:flutter/services.dart'; 14 | 15 | part 'beacon/authorization_status.dart'; 16 | part 'beacon/beacon.dart'; 17 | part 'beacon/beacon_broadcast.dart'; 18 | part 'beacon/bluetooth_state.dart'; 19 | part 'beacon/monitoring_result.dart'; 20 | part 'beacon/ranging_result.dart'; 21 | part 'beacon/region.dart'; 22 | 23 | /// Singleton instance for accessing scanning API. 24 | final FlutterBeacon flutterBeacon = new FlutterBeacon._internal(); 25 | 26 | /// Provide iBeacon scanning API for both Android and iOS. 27 | class FlutterBeacon { 28 | FlutterBeacon._internal(); 29 | 30 | /// Method Channel used to communicate to native code. 31 | static const MethodChannel _methodChannel = 32 | const MethodChannel('flutter_beacon'); 33 | 34 | /// Event Channel used to communicate to native code ranging beacons. 35 | static const EventChannel _rangingChannel = 36 | EventChannel('flutter_beacon_event'); 37 | 38 | /// Event Channel used to communicate to native code monitoring beacons. 39 | static const EventChannel _monitoringChannel = 40 | EventChannel('flutter_beacon_event_monitoring'); 41 | 42 | /// Event Channel used to communicate to native code to checking 43 | /// for bluetooth state changed. 44 | static const EventChannel _bluetoothStateChangedChannel = 45 | EventChannel('flutter_bluetooth_state_changed'); 46 | 47 | /// Event Channel used to communicate to native code to checking 48 | /// for bluetooth state changed. 49 | static const EventChannel _authorizationStatusChangedChannel = 50 | EventChannel('flutter_authorization_status_changed'); 51 | 52 | /// This information does not change from call to call. Cache it. 53 | Stream? _onBluetoothState; 54 | 55 | /// This information does not change from call to call. Cache it. 56 | Stream? _onAuthorizationStatus; 57 | 58 | /// Initialize scanning API. 59 | Future get initializeScanning async { 60 | final result = await _methodChannel.invokeMethod('initialize'); 61 | 62 | if (result is bool) { 63 | return result; 64 | } else if (result is int) { 65 | return result == 1; 66 | } 67 | 68 | return result; 69 | } 70 | 71 | /// Initialize scanning API and check required permissions. 72 | /// 73 | /// For Android, it will check whether Bluetooth is enabled, 74 | /// allowed to access location services and check 75 | /// whether location services is enabled. 76 | /// For iOS, it will check whether Bluetooth is enabled, 77 | /// requestWhenInUse or requestAlways location services and check 78 | /// whether location services is enabled. 79 | Future get initializeAndCheckScanning async { 80 | final result = await _methodChannel.invokeMethod('initializeAndCheck'); 81 | 82 | if (result is bool) { 83 | return result; 84 | } 85 | 86 | return result == 1; 87 | } 88 | 89 | /// Set the default AuthorizationStatus to use in requesting location authorization. 90 | /// For iOS, this can be either [AuthorizationStatus.whenInUse] or [AuthorizationStatus.always]. 91 | /// For Android, this is not used. 92 | /// 93 | /// This method should be called very early to have an effect, 94 | /// before any of the other initializeScanning or authorizationStatus getters. 95 | /// 96 | Future setLocationAuthorizationTypeDefault( 97 | AuthorizationStatus authorizationStatus) async { 98 | return await _methodChannel.invokeMethod( 99 | 'setLocationAuthorizationTypeDefault', authorizationStatus.value); 100 | } 101 | 102 | /// Check for the latest [AuthorizationStatus] from device. 103 | /// 104 | /// For Android, this will return [AuthorizationStatus.allowed], [AuthorizationStatus.denied] or [AuthorizationStatus.notDetermined]. 105 | Future get authorizationStatus async { 106 | final status = await _methodChannel.invokeMethod('authorizationStatus'); 107 | return AuthorizationStatus.parse(status); 108 | } 109 | 110 | /// Return `true` when location service is enabled, otherwise `false`. 111 | Future get checkLocationServicesIfEnabled async { 112 | final result = 113 | await _methodChannel.invokeMethod('checkLocationServicesIfEnabled'); 114 | 115 | if (result is bool) { 116 | return result; 117 | } 118 | 119 | return result == 1; 120 | } 121 | 122 | /// Check for the latest [BluetoothState] from device. 123 | Future get bluetoothState async { 124 | final status = await _methodChannel.invokeMethod('bluetoothState'); 125 | return BluetoothState.parse(status); 126 | } 127 | 128 | /// Request an authorization to the device. 129 | /// 130 | /// For Android, this will request a permission of `Manifest.permission.ACCESS_COARSE_LOCATION`. 131 | /// For iOS, this will send a request `CLLocationManager#requestAlwaysAuthorization`. 132 | Future get requestAuthorization async { 133 | final result = await _methodChannel.invokeMethod('requestAuthorization'); 134 | 135 | if (result is bool) { 136 | return result; 137 | } 138 | 139 | return result == 1; 140 | } 141 | 142 | /// Request to open Bluetooth Settings from device. 143 | /// 144 | /// For iOS, this will does nothing because of private method. 145 | Future get openBluetoothSettings async { 146 | final result = await _methodChannel.invokeMethod('openBluetoothSettings'); 147 | 148 | if (result is bool) { 149 | return result; 150 | } 151 | 152 | return result == 1; 153 | } 154 | 155 | /// Request to open Locations Settings from device. 156 | /// 157 | /// For iOS, this will does nothing because of private method. 158 | Future get openLocationSettings async { 159 | final result = await _methodChannel.invokeMethod('openLocationSettings'); 160 | 161 | if (result is bool) { 162 | return result; 163 | } 164 | 165 | return result == 1; 166 | } 167 | 168 | /// Request to open Application Settings from device. 169 | /// 170 | /// For Android, this will does nothing. 171 | Future get openApplicationSettings async { 172 | final result = await _methodChannel.invokeMethod('openApplicationSettings'); 173 | 174 | if (result is bool) { 175 | return result; 176 | } 177 | 178 | return result == 1; 179 | } 180 | 181 | /// Customize duration of the beacon scan on the Android Platform. 182 | Future setScanPeriod(int scanPeriod) async { 183 | return await _methodChannel 184 | .invokeMethod('setScanPeriod', {"scanPeriod": scanPeriod}); 185 | } 186 | 187 | /// Customize duration spent not scanning between each scan cycle on the Android Platform. 188 | Future setBetweenScanPeriod(int scanPeriod) async { 189 | return await _methodChannel.invokeMethod( 190 | 'setBetweenScanPeriod', {"betweenScanPeriod": scanPeriod}); 191 | } 192 | 193 | /// Close scanning API. 194 | Future get close async { 195 | final result = await _methodChannel.invokeMethod('close'); 196 | 197 | if (result is bool) { 198 | return result; 199 | } 200 | 201 | return result == 1; 202 | } 203 | 204 | /// Start ranging iBeacons with defined [List] of [Region]s. 205 | /// 206 | /// This will fires [RangingResult] whenever the iBeacons in range. 207 | Stream ranging(List regions) { 208 | final list = regions.map((region) => region.toJson).toList(); 209 | final Stream onRanging = _rangingChannel 210 | .receiveBroadcastStream(list) 211 | .map((dynamic event) => RangingResult.from(event)); 212 | return onRanging; 213 | } 214 | 215 | /// Start monitoring iBeacons with defined [List] of [Region]s. 216 | /// 217 | /// This will fires [MonitoringResult] whenever the iBeacons in range. 218 | Stream monitoring(List regions) { 219 | final list = regions.map((region) => region.toJson).toList(); 220 | final Stream onMonitoring = _monitoringChannel 221 | .receiveBroadcastStream(list) 222 | .map((dynamic event) => MonitoringResult.from(event)); 223 | return onMonitoring; 224 | } 225 | 226 | /// Start checking for bluetooth state changed. 227 | /// 228 | /// This will fires [BluetoothState] whenever bluetooth state changed. 229 | Stream bluetoothStateChanged() { 230 | if (_onBluetoothState == null) { 231 | _onBluetoothState = _bluetoothStateChangedChannel 232 | .receiveBroadcastStream() 233 | .map((dynamic event) => BluetoothState.parse(event)); 234 | } 235 | return _onBluetoothState!; 236 | } 237 | 238 | /// Start checking for location service authorization status changed. 239 | /// 240 | /// This will fires [AuthorizationStatus] whenever authorization status changed. 241 | Stream authorizationStatusChanged() { 242 | if (_onAuthorizationStatus == null) { 243 | _onAuthorizationStatus = _authorizationStatusChangedChannel 244 | .receiveBroadcastStream() 245 | .map((dynamic event) => AuthorizationStatus.parse(event)); 246 | } 247 | return _onAuthorizationStatus!; 248 | } 249 | 250 | Future startBroadcast(BeaconBroadcast params) async { 251 | await _methodChannel.invokeMethod('startBroadcast', params.toJson); 252 | } 253 | 254 | Future stopBroadcast() async { 255 | await _methodChannel.invokeMethod('stopBroadcast'); 256 | } 257 | 258 | Future isBroadcasting() async { 259 | final flag = await _methodChannel.invokeMethod('isBroadcasting'); 260 | return flag == true || flag == 1; 261 | } 262 | 263 | Future isBroadcastSupported() async { 264 | final flag = await _methodChannel.invokeMethod('isBroadcastSupported'); 265 | return flag == true || flag == 1; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /android/src/main/java/com/flutterbeacon/FlutterBeaconScanner.java: -------------------------------------------------------------------------------- 1 | package com.flutterbeacon; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.RemoteException; 8 | import android.os.Looper; 9 | import android.os.Handler; 10 | import android.util.Log; 11 | 12 | import org.altbeacon.beacon.Beacon; 13 | import org.altbeacon.beacon.BeaconConsumer; 14 | import org.altbeacon.beacon.MonitorNotifier; 15 | import org.altbeacon.beacon.RangeNotifier; 16 | import org.altbeacon.beacon.Region; 17 | 18 | import java.lang.ref.WeakReference; 19 | import java.util.ArrayList; 20 | import java.util.Collection; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import io.flutter.plugin.common.EventChannel; 26 | 27 | class FlutterBeaconScanner { 28 | private static final String TAG = FlutterBeaconScanner.class.getSimpleName(); 29 | private final FlutterBeaconPlugin plugin; 30 | private final WeakReference activity; 31 | 32 | private Handler handler; 33 | 34 | private EventChannel.EventSink eventSinkRanging; 35 | private EventChannel.EventSink eventSinkMonitoring; 36 | private List regionRanging; 37 | private List regionMonitoring; 38 | 39 | public FlutterBeaconScanner(FlutterBeaconPlugin plugin, Activity activity) { 40 | this.plugin = plugin; 41 | this.activity = new WeakReference<>(activity); 42 | handler = new Handler(Looper.getMainLooper()); 43 | } 44 | 45 | final EventChannel.StreamHandler rangingStreamHandler = new EventChannel.StreamHandler() { 46 | @Override 47 | public void onListen(Object o, EventChannel.EventSink eventSink) { 48 | Log.d("RANGING", "Start ranging = " + o); 49 | startRanging(o, eventSink); 50 | } 51 | 52 | @Override 53 | public void onCancel(Object o) { 54 | Log.d("RANGING", "Stop ranging = " + o); 55 | stopRanging(); 56 | } 57 | }; 58 | 59 | @SuppressWarnings("rawtypes") 60 | private void startRanging(Object o, EventChannel.EventSink eventSink) { 61 | if (o instanceof List) { 62 | List list = (List) o; 63 | if (regionRanging == null) { 64 | regionRanging = new ArrayList<>(); 65 | } else { 66 | regionRanging.clear(); 67 | } 68 | for (Object object : list) { 69 | if (object instanceof Map) { 70 | Map map = (Map) object; 71 | Region region = FlutterBeaconUtils.regionFromMap(map); 72 | if (region != null) { 73 | regionRanging.add(region); 74 | } 75 | } 76 | } 77 | } else { 78 | eventSink.error("Beacon", "invalid region for ranging", null); 79 | return; 80 | } 81 | eventSinkRanging = eventSink; 82 | if (plugin.getBeaconManager() != null && !plugin.getBeaconManager().isBound(beaconConsumer)) { 83 | plugin.getBeaconManager().bind(beaconConsumer); 84 | } else { 85 | startRanging(); 86 | } 87 | } 88 | 89 | void startRanging() { 90 | if (regionRanging == null || regionRanging.isEmpty()) { 91 | Log.e("RANGING", "Region ranging is null or empty. Ranging not started."); 92 | return; 93 | } 94 | 95 | try { 96 | if (plugin.getBeaconManager() != null) { 97 | plugin.getBeaconManager().removeAllRangeNotifiers(); 98 | plugin.getBeaconManager().addRangeNotifier(rangeNotifier); 99 | for (Region region : regionRanging) { 100 | plugin.getBeaconManager().startRangingBeaconsInRegion(region); 101 | } 102 | } 103 | } catch (RemoteException e) { 104 | if (eventSinkRanging != null) { 105 | eventSinkRanging.error("Beacon", e.getLocalizedMessage(), null); 106 | } 107 | } 108 | } 109 | 110 | void stopRanging() { 111 | if (regionRanging != null && !regionRanging.isEmpty()) { 112 | try { 113 | for (Region region : regionRanging) { 114 | plugin.getBeaconManager().stopRangingBeaconsInRegion(region); 115 | } 116 | 117 | plugin.getBeaconManager().removeRangeNotifier(rangeNotifier); 118 | } catch (RemoteException ignored) { 119 | } 120 | } 121 | eventSinkRanging = null; 122 | } 123 | 124 | private final RangeNotifier rangeNotifier = new RangeNotifier() { 125 | @Override 126 | public void didRangeBeaconsInRegion(Collection collection, Region region) { 127 | if (eventSinkRanging != null) { 128 | final Map map = new HashMap<>(); 129 | map.put("region", FlutterBeaconUtils.regionToMap(region)); 130 | map.put("beacons", FlutterBeaconUtils.beaconsToArray(new ArrayList<>(collection))); 131 | handler.post(new Runnable(){ 132 | @Override 133 | public void run() { 134 | if(eventSinkRanging != null){ 135 | eventSinkRanging.success(map); 136 | } 137 | } 138 | }); 139 | } 140 | } 141 | }; 142 | 143 | final EventChannel.StreamHandler monitoringStreamHandler = new EventChannel.StreamHandler() { 144 | @Override 145 | public void onListen(Object o, EventChannel.EventSink eventSink) { 146 | startMonitoring(o, eventSink); 147 | } 148 | 149 | @Override 150 | public void onCancel(Object o) { 151 | stopMonitoring(); 152 | } 153 | }; 154 | 155 | @SuppressWarnings("rawtypes") 156 | private void startMonitoring(Object o, EventChannel.EventSink eventSink) { 157 | Log.d(TAG, "START MONITORING=" + o); 158 | if (o instanceof List) { 159 | List list = (List) o; 160 | if (regionMonitoring == null) { 161 | regionMonitoring = new ArrayList<>(); 162 | } else { 163 | regionMonitoring.clear(); 164 | } 165 | for (Object object : list) { 166 | if (object instanceof Map) { 167 | Map map = (Map) object; 168 | Region region = FlutterBeaconUtils.regionFromMap(map); 169 | regionMonitoring.add(region); 170 | } 171 | } 172 | } else { 173 | eventSink.error("Beacon", "invalid region for monitoring", null); 174 | return; 175 | } 176 | eventSinkMonitoring = eventSink; 177 | if (plugin.getBeaconManager() != null && !plugin.getBeaconManager().isBound(beaconConsumer)) { 178 | plugin.getBeaconManager().bind(beaconConsumer); 179 | } else { 180 | startMonitoring(); 181 | } 182 | } 183 | 184 | void startMonitoring() { 185 | if (regionMonitoring == null || regionMonitoring.isEmpty()) { 186 | Log.e("MONITORING", "Region monitoring is null or empty. Monitoring not started."); 187 | return; 188 | } 189 | 190 | try { 191 | plugin.getBeaconManager().removeAllMonitorNotifiers(); 192 | plugin.getBeaconManager().addMonitorNotifier(monitorNotifier); 193 | for (Region region : regionMonitoring) { 194 | plugin.getBeaconManager().startMonitoringBeaconsInRegion(region); 195 | } 196 | } catch (RemoteException e) { 197 | if (eventSinkMonitoring != null) { 198 | eventSinkMonitoring.error("Beacon", e.getLocalizedMessage(), null); 199 | } 200 | } 201 | } 202 | 203 | void stopMonitoring() { 204 | if (regionMonitoring != null && !regionMonitoring.isEmpty()) { 205 | try { 206 | for (Region region : regionMonitoring) { 207 | plugin.getBeaconManager().stopMonitoringBeaconsInRegion(region); 208 | } 209 | plugin.getBeaconManager().removeMonitorNotifier(monitorNotifier); 210 | } catch (RemoteException ignored) { 211 | } 212 | } 213 | eventSinkMonitoring = null; 214 | } 215 | 216 | private final MonitorNotifier monitorNotifier = new MonitorNotifier() { 217 | @Override 218 | public void didEnterRegion(Region region) { 219 | if (eventSinkMonitoring != null) { 220 | final Map map = new HashMap<>(); 221 | map.put("event", "didEnterRegion"); 222 | map.put("region", FlutterBeaconUtils.regionToMap(region)); 223 | handler.post(new Runnable(){ 224 | @Override 225 | public void run() { 226 | if(eventSinkMonitoring != null){ 227 | eventSinkMonitoring.success(map); 228 | } 229 | } 230 | }); 231 | } 232 | } 233 | 234 | @Override 235 | public void didExitRegion(Region region) { 236 | if (eventSinkMonitoring != null) { 237 | final Map map = new HashMap<>(); 238 | map.put("event", "didExitRegion"); 239 | map.put("region", FlutterBeaconUtils.regionToMap(region)); 240 | handler.post(new Runnable(){ 241 | @Override 242 | public void run() { 243 | if(eventSinkMonitoring != null){ 244 | eventSinkMonitoring.success(map); 245 | } 246 | } 247 | }); 248 | } 249 | } 250 | 251 | @Override 252 | public void didDetermineStateForRegion(int state, Region region) { 253 | if (eventSinkMonitoring != null) { 254 | final Map map = new HashMap<>(); 255 | map.put("event", "didDetermineStateForRegion"); 256 | map.put("state", FlutterBeaconUtils.parseState(state)); 257 | map.put("region", FlutterBeaconUtils.regionToMap(region)); 258 | handler.post(new Runnable(){ 259 | @Override 260 | public void run() { 261 | if(eventSinkMonitoring != null){ 262 | eventSinkMonitoring.success(map); 263 | } 264 | } 265 | }); 266 | } 267 | } 268 | }; 269 | 270 | final BeaconConsumer beaconConsumer = new BeaconConsumer() { 271 | @Override 272 | public void onBeaconServiceConnect() { 273 | if (plugin.flutterResult != null) { 274 | plugin.flutterResult.success(true); 275 | plugin.flutterResult = null; 276 | } else { 277 | startRanging(); 278 | startMonitoring(); 279 | } 280 | } 281 | 282 | @Override 283 | public Context getApplicationContext() { 284 | return activity.get().getApplicationContext(); 285 | } 286 | 287 | @Override 288 | public void unbindService(ServiceConnection serviceConnection) { 289 | activity.get().unbindService(serviceConnection); 290 | } 291 | 292 | @Override 293 | public boolean bindService(Intent intent, ServiceConnection serviceConnection, int i) { 294 | return activity.get().bindService(intent, serviceConnection, i); 295 | } 296 | }; 297 | } 298 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. --------------------------------------------------------------------------------