diff --git a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java index f3ffbdb9..28513667 100644 --- a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java +++ b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java @@ -58,10 +58,11 @@ import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener; +import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; /** FlutterBluePlugin */ -public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, RequestPermissionsResultListener { +public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, RequestPermissionsResultListener, ActivityResultListener { private static final String TAG = "FlutterBluePlugin"; private Object initializationLock = new Object(); private Context context; @@ -78,6 +79,7 @@ public class FlutterBluePlugin implements FlutterPlugin, ActivityAware, MethodCa private Activity activity; private static final int REQUEST_FINE_LOCATION_PERMISSIONS = 1452; + private static final int REQUEST_ENABLE_BT = 1453; static final private UUID CCCD_ID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private final Map mDevices = new HashMap<>(); private LogLevel logLevel = LogLevel.EMERGENCY; @@ -158,9 +160,11 @@ private void setup( if (registrar != null) { // V1 embedding setup for activity listeners. registrar.addRequestPermissionsResultListener(this); + registrar.addActivityResultListener(this); } else { // V2 embedding setup for activity listeners. activityBinding.addRequestPermissionsResultListener(this); + activityBinding.addActivityResultListener(this); } } } @@ -236,6 +240,24 @@ public void onMethodCall(MethodCall call, Result result) { break; } + case "enable": + { + if (!mBluetoothAdapter.isEnabled()) { + boolean ask = (boolean)(call.arguments); + if (ask) { + activity.startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BT); + result.success(null); + } + else { + result.success(mBluetoothAdapter.enable()); + } + } + else { + result.success(true); + } + break; + } + case "startScan": { if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) @@ -652,6 +674,15 @@ public boolean onRequestPermissionsResult( return false; } + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_ENABLE_BT) { + channel.invokeMethod("EnableResult", resultCode == Activity.RESULT_OK); + return true; + } + return false; + } + private BluetoothGatt locateGatt(String remoteId) throws Exception { BluetoothDeviceCache cache = mDevices.get(remoteId); if(cache == null || cache.gatt == null) { diff --git a/example/ios/Podfile b/example/ios/Podfile index 98a90b8a..f7d6a5e6 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -10,78 +10,29 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end - end - generated_key_values -end - -target 'Runner' do - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - # Plugin Pods +flutter_ios_podfile_setup - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true - post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0fb4b650..6d25a6b6 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,4 @@ PODS: - - e2e (0.0.1): - - Flutter - Flutter (1.0.0) - flutter_blue (0.0.1): - Flutter @@ -11,7 +9,6 @@ PODS: - Protobuf (3.11.4) DEPENDENCIES: - - e2e (from `.symlinks/plugins/e2e/ios`) - Flutter (from `Flutter`) - flutter_blue (from `.symlinks/plugins/flutter_blue/ios`) @@ -20,19 +17,16 @@ SPEC REPOS: - Protobuf EXTERNAL SOURCES: - e2e: - :path: ".symlinks/plugins/e2e/ios" Flutter: :path: Flutter flutter_blue: :path: ".symlinks/plugins/flutter_blue/ios" SPEC CHECKSUMS: - e2e: 967b9b1fc533b7636a3b7a719f840c27f301fe1f - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_blue: eeb381dc4727a0954dede73515f683865494b370 Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 -PODFILE CHECKSUM: 3dbe063e9c90a5d7c9e4e76e70a821b9e2c1d271 +PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d -COCOAPODS: 1.9.1 +COCOAPODS: 1.10.1 diff --git a/example/lib/main.dart b/example/lib/main.dart index e4a55a1a..e0074cc1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -41,23 +41,29 @@ class BluetoothOffScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.lightBlue, - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.bluetooth_disabled, - size: 200.0, - color: Colors.white54, - ), - Text( - 'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.', - style: Theme.of(context) - .primaryTextTheme - .subhead - ?.copyWith(color: Colors.white), - ), - ], + body: InkWell( + onTap: () { + FlutterBlue.instance.enable(ask: true) + .then((value) => print('Bluetooth enable returned $value')); + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.bluetooth_disabled, + size: 200.0, + color: Colors.white54, + ), + Text( + 'Bluetooth Adapter is ${state != null ? state.toString().substring(15) : 'not available'}.', + style: Theme.of(context) + .primaryTextTheme + .subhead + ?.copyWith(color: Colors.white), + ), + ], + ), ), ), ); diff --git a/ios/Classes/FlutterBluePlugin.m b/ios/Classes/FlutterBluePlugin.m index 65a73b6d..f6f5693f 100644 --- a/ios/Classes/FlutterBluePlugin.m +++ b/ios/Classes/FlutterBluePlugin.m @@ -82,6 +82,14 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else { result(@(NO)); } + } else if([@"enable" isEqualToString:call.method]) { + // Enabling Bluetooth programmatically is not possible on iOS. + // If it is already enabled, we still consider it successful to match Android behaviour + if(self.centralManager.state == CBManagerStatePoweredOn) { + result(@(YES)); + } else { + result(@(NO)); + } } else if([@"startScan" isEqualToString:call.method]) { // Clear any existing scan results [self.scannedPeripherals removeAllObjects]; diff --git a/lib/src/flutter_blue.dart b/lib/src/flutter_blue.dart index 6d2f76e9..0a1f171e 100644 --- a/lib/src/flutter_blue.dart +++ b/lib/src/flutter_blue.dart @@ -64,6 +64,28 @@ class FlutterBlue { .map((s) => BluetoothState.values[s.state.value]); } + /// Turn on Bluetooth module. + /// + /// On Android, this is done silently by default (needs `android.permission.BLUETOOTH_ADMIN` permission). + /// If [ask] is true, a system dialog asking for permission is shown. + /// + /// On iOS, the functionality is not supported by the operating system and [enable] will return false if the Bluetooth is turned off + /// and true if it is on. + Future enable({bool ask = false}) async { + var result = await _channel + .invokeMethod('enable', ask); + if (result == null) { + return FlutterBlue.instance._methodStream + .where((m) => m.method == "EnableResult") + .map((m) => m.arguments) + .first + .then((c) { + return (c); + }); + } + return result; + } + /// Retrieve a list of connected devices Future> get connectedDevices { return _channel