diff --git a/packages/wifi_info_flutter/wifi_info_flutter/README.md b/packages/wifi_info_flutter/wifi_info_flutter/README.md index cf33a8e82532..0c2eff383c16 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/README.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/README.md @@ -1,15 +1,71 @@ # wifi_info_flutter -A new flutter plugin project. +This plugin retrieves information about a device's connection to wifi. -## Getting Started +> Note that on Android, this does not guarantee connection to Internet. For instance, +the app might have wifi access but it might be a VPN or a hotel WiFi with no access. + +## Usage + +### Android + +Sample usage to check current status: + +To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, ensure all of the following conditions are met: + + * If your app is targeting Android 10 (API level 29) SDK or higher, your app has the ACCESS_FINE_LOCATION permission. + + * If your app is targeting SDK lower than Android 10 (API level 29), your app has the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission. + + * Location services are enabled on the device (under Settings > Location). + +You can get wi-fi related information using: + +```dart +import 'package:wifi_info_flutter/wifi_info_flutter.dart'; + +var wifiBSSID = await WifiFlutter().getWifiBSSID(); +var wifiIP = await WifiFlutter().getWifiIP(); +var wifiName = await WifiFlutter().getWifiName(); +``` + +### iOS 12 + +To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null. + +### iOS 13 -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. + +As of iOS 13, Apple announced that these APIs will no longer return valid information. +An app linked against iOS 12 or earlier receives pseudo-values such as: + + * SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU). + + * BSSID: "00:00:00:00:00:00" + +An app linked against iOS 13 or later receives `null`. + +The `CNCopyCurrentNetworkInfo` will work for Apps that: + + * The app uses Core Location, and has the user’s authorization to use location information. + + * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + + * The app has active VPN configurations installed. + +If your app falls into the last two categories, it will work as it is. If your app doesn't fall into the last two categories, +and you still need to access the wifi information, you should request user's authorization to use location information. + +There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`. +To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + +* `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. +* `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. + +## Getting Started -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). +For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml index 730c0a71943c..03ac924f9427 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java new file mode 100644 index 000000000000..f74e4f0f75e1 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java @@ -0,0 +1,56 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.wifi_info_flutter; + +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +/** Reports wifi information. */ +class WifiInfoFlutter { + private WifiManager wifiManager; + + WifiInfoFlutter(WifiManager wifiManager) { + this.wifiManager = wifiManager; + } + + String getWifiName() { + final WifiInfo wifiInfo = getWifiInfo(); + String ssid = null; + if (wifiInfo != null) ssid = wifiInfo.getSSID(); + if (ssid != null) ssid = ssid.replaceAll("\"", ""); // Android returns "SSID" + if (ssid != null && ssid.equals("")) ssid = null; + return ssid; + } + + String getWifiBSSID() { + final WifiInfo wifiInfo = getWifiInfo(); + String bssid = null; + if (wifiInfo != null) { + bssid = wifiInfo.getBSSID(); + } + return bssid; + } + + String getWifiIPAddress() { + WifiInfo wifiInfo = null; + if (wifiManager != null) wifiInfo = wifiManager.getConnectionInfo(); + + String ip = null; + int i_ip = 0; + if (wifiInfo != null) i_ip = wifiInfo.getIpAddress(); + + if (i_ip != 0) + ip = + String.format( + "%d.%d.%d.%d", + (i_ip & 0xff), (i_ip >> 8 & 0xff), (i_ip >> 16 & 0xff), (i_ip >> 24 & 0xff)); + + return ip; + } + + private WifiInfo getWifiInfo() { + return wifiManager == null ? null : wifiManager.getConnectionInfo(); + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java new file mode 100644 index 000000000000..b996d32255e3 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java @@ -0,0 +1,44 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.wifi_info_flutter; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * The handler receives {@link MethodCall}s from the UIThread, gets the related information from + * a @{@link WifiInfoFlutter}, and then send the result back to the UIThread through the {@link + * MethodChannel.Result}. + */ +class WifiInfoFlutterMethodChannelHandler implements MethodChannel.MethodCallHandler { + private WifiInfoFlutter wifiInfoFlutter; + + /** + * Construct the WifiInfoFlutterMethodChannelHandler with a {@code wifiInfoFlutter}. The {@code + * wifiInfoFlutter} must not be null. + */ + WifiInfoFlutterMethodChannelHandler(WifiInfoFlutter wifiInfoFlutter) { + assert (wifiInfoFlutter != null); + this.wifiInfoFlutter = wifiInfoFlutter; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "wifiName": + result.success(wifiInfoFlutter.getWifiName()); + break; + case "wifiBSSID": + result.success(wifiInfoFlutter.getWifiBSSID()); + break; + case "wifiIPAddress": + result.success(wifiInfoFlutter.getWifiIPAddress()); + break; + default: + result.notImplemented(); + break; + } + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java index 791d88c4b14e..407bbc0dd560 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java @@ -1,37 +1,46 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.wifi_info_flutter; -import androidx.annotation.NonNull; +import android.content.Context; +import android.net.wifi.WifiManager; import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; /** WifiInfoFlutterPlugin */ -public class WifiInfoFlutterPlugin implements FlutterPlugin, MethodCallHandler { - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private MethodChannel channel; +public class WifiInfoFlutterPlugin implements FlutterPlugin { + private MethodChannel methodChannel; - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { - channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "wifi_info_flutter"); - channel.setMethodCallHandler(this); + /** Plugin registration. */ + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + WifiInfoFlutterPlugin plugin = new WifiInfoFlutterPlugin(); + plugin.setupChannels(registrar.messenger(), registrar.context()); } @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { - if (call.method.equals("getPlatformVersion")) { - result.success("Android " + android.os.Build.VERSION.RELEASE); - } else { - result.notImplemented(); - } + public void onAttachedToEngine(FlutterPluginBinding binding) { + setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); } @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - channel.setMethodCallHandler(null); + public void onDetachedFromEngine(FlutterPluginBinding binding) { + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + + private void setupChannels(BinaryMessenger messenger, Context context) { + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/wifi_flutter_info"); + final WifiManager wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + + final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager); + + final WifiInfoFlutterMethodChannelHandler methodChannelHandler = + new WifiInfoFlutterMethodChannelHandler(wifiInfoFlutter); + methodChannel.setMethodCallHandler(methodChannelHandler); } } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties index 94adc3a3f97a..a6738207fd15 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.enableR8=true diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist index 11cd5cfedbed..2c7f6b8c6b85 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + NSLocationAlwaysAndWhenInUseUsageDescription + This app requires accessing your location information all the time to get wi-fi information. + NSLocationWhenInUseUsageDescription + This app requires accessing your location information when the app is in foreground to get wi-fi information. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart index b9b8f6b20df7..17a9e76ad946 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart @@ -1,60 +1,173 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + // ignore_for_file: public_member_api_docs -import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:io'; +import 'package:connectivity/connectivity.dart' + show Connectivity, ConnectivityResult; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:wifi_info_flutter/wifi_info_flutter.dart'; +// Sets a platform override for desktop to avoid exceptions. See +// https://flutter.dev/desktop#target-platform-override for more info. +void _enablePlatformOverrideForDesktop() { + if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + } +} + void main() { + _enablePlatformOverrideForDesktop(); runApp(MyApp()); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + + final String title; + @override - _MyAppState createState() => _MyAppState(); + _MyHomePageState createState() => _MyHomePageState(); } -class _MyAppState extends State { - String _platformVersion = 'Unknown'; +class _MyHomePageState extends State { + String _connectionStatus = 'Unknown'; + final Connectivity _connectivity = Connectivity(); + final WifiInfo _wifiInfo = WifiInfo(); + StreamSubscription _connectivitySubscription; @override void initState() { super.initState(); - initPlatformState(); + initConnectivity(); + _connectivitySubscription = + _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); + } + + @override + void dispose() { + _connectivitySubscription.cancel(); + super.dispose(); } // Platform messages are asynchronous, so we initialize in an async method. - Future initPlatformState() async { - String platformVersion; + Future initConnectivity() async { + ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { - platformVersion = await WifiInfoFlutter.platformVersion; - } on PlatformException { - platformVersion = 'Failed to get platform version.'; + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + print(e.toString()); } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. - if (!mounted) return; + if (!mounted) { + return Future.value(null); + } - setState(() { - _platformVersion = platformVersion; - }); + return _updateConnectionStatus(result); } @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Running on: $_platformVersion\n'), - ), + return Scaffold( + appBar: AppBar( + title: const Text('Connectivity example app'), ), + body: Center(child: Text('Connection Status: $_connectionStatus')), ); } + + Future _updateConnectionStatus(ConnectivityResult result) async { + switch (result) { + case ConnectivityResult.wifi: + String wifiName, wifiBSSID, wifiIP; + + try { + if (!kIsWeb && Platform.isIOS) { + LocationAuthorizationStatus status = + await _wifiInfo.getLocationServiceAuthorization(); + if (status == LocationAuthorizationStatus.notDetermined) { + status = await _wifiInfo.requestLocationServiceAuthorization(); + } + if (status == LocationAuthorizationStatus.authorizedAlways || + status == LocationAuthorizationStatus.authorizedWhenInUse) { + wifiName = await _connectivity.getWifiName(); + } else { + wifiName = await _connectivity.getWifiName(); + } + } else { + wifiName = await _connectivity.getWifiName(); + } + } on PlatformException catch (e) { + print(e.toString()); + wifiName = "Failed to get Wifi Name"; + } + + try { + if (!kIsWeb && Platform.isIOS) { + LocationAuthorizationStatus status = + await _wifiInfo.getLocationServiceAuthorization(); + if (status == LocationAuthorizationStatus.notDetermined) { + status = await _wifiInfo.requestLocationServiceAuthorization(); + } + if (status == LocationAuthorizationStatus.authorizedAlways || + status == LocationAuthorizationStatus.authorizedWhenInUse) { + wifiBSSID = await _connectivity.getWifiBSSID(); + } else { + wifiBSSID = await _connectivity.getWifiBSSID(); + } + } else { + wifiBSSID = await _connectivity.getWifiBSSID(); + } + } on PlatformException catch (e) { + print(e.toString()); + wifiBSSID = "Failed to get Wifi BSSID"; + } + + try { + wifiIP = await _connectivity.getWifiIP(); + } on PlatformException catch (e) { + print(e.toString()); + wifiIP = "Failed to get Wifi IP"; + } + + setState(() { + _connectionStatus = '$result\n' + 'Wifi Name: $wifiName\n' + 'Wifi BSSID: $wifiBSSID\n' + 'Wifi IP: $wifiIP\n'; + }); + break; + case ConnectivityResult.mobile: + case ConnectivityResult.none: + setState(() => _connectionStatus = result.toString()); + break; + default: + setState(() => _connectionStatus = 'Failed to get connectivity.'); + break; + } + } } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml index 1bf8a8d5dbf3..7ebc1573ddcf 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml @@ -1,71 +1,23 @@ name: wifi_info_flutter_example description: Demonstrates how to use the wifi_info_flutter plugin. - -# The following line prevents the package from being accidentally published to -# pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' environment: sdk: ">=2.7.0 <3.0.0" dependencies: + connectivity: 0.4.9+3 flutter: sdk: flutter - wifi_info_flutter: - # When depending on this package from a real application you should use: - # wifi_info_flutter: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 dev_dependencies: + integration_test: + path: ../../../integration_test flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart new file mode 100644 index 000000000000..103dc54a1eaa --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart @@ -0,0 +1,27 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:wifi_info_flutter/wifi_info_flutter.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('$WifiInfo test driver', () { + WifiInfo _wifiInfo; + + setUpAll(() async { + _wifiInfo = WifiInfo(); + }); + + testWidgets('test location methods, iOS only', (WidgetTester tester) async { + expect( + (await _wifiInfo.getLocationServiceAuthorization()), + LocationAuthorizationStatus.notDetermined, + ); + }, skip: !Platform.isIOS); + }); +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart new file mode 100644 index 000000000000..8a77ec87bbac --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart @@ -0,0 +1,16 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + final String data = + await driver.requestData(null, timeout: const Duration(minutes: 1)); + await driver.close(); + final Map result = jsonDecode(data); + exit(result['result'] == 'true' ? 0 : 1); +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart new file mode 100644 index 000000000000..103dc54a1eaa --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart @@ -0,0 +1,27 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:wifi_info_flutter/wifi_info_flutter.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('$WifiInfo test driver', () { + WifiInfo _wifiInfo; + + setUpAll(() async { + _wifiInfo = WifiInfo(); + }); + + testWidgets('test location methods, iOS only', (WidgetTester tester) async { + expect( + (await _wifiInfo.getLocationServiceAuthorization()), + LocationAuthorizationStatus.notDetermined, + ); + }, skip: !Platform.isIOS); + }); +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h new file mode 100644 index 000000000000..7cfe42df6079 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.h @@ -0,0 +1,23 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class FLTWifiInfoLocationDelegate; + +typedef void (^FLTWifiInfoLocationCompletion)(CLAuthorizationStatus); + +@interface FLTWifiInfoLocationHandler : NSObject + ++ (CLAuthorizationStatus)locationAuthorizationStatus; + +- (void)requestLocationAuthorization:(BOOL)always + completion:(_Nonnull FLTWifiInfoLocationCompletion)completionHnadler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m new file mode 100644 index 000000000000..2f237a1d3f15 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m @@ -0,0 +1,58 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWifiInfoLocationHandler.h" + +@interface FLTWifiInfoLocationHandler () + +@property(copy, nonatomic) FLTWifiInfoLocationCompletion completion; +@property(strong, nonatomic) CLLocationManager *locationManager; + +@end + +@implementation FLTWifiInfoLocationHandler + ++ (CLAuthorizationStatus)locationAuthorizationStatus { + return CLLocationManager.authorizationStatus; +} + +- (void)requestLocationAuthorization:(BOOL)always + completion:(FLTWifiInfoLocationCompletion)completionHandler { + CLAuthorizationStatus status = CLLocationManager.authorizationStatus; + if (status != kCLAuthorizationStatusAuthorizedWhenInUse && always) { + completionHandler(kCLAuthorizationStatusDenied); + return; + } else if (status != kCLAuthorizationStatusNotDetermined) { + completionHandler(status); + return; + } + + if (self.completion) { + // If a request is still in process, immediately return. + completionHandler(kCLAuthorizationStatusNotDetermined); + return; + } + + self.completion = completionHandler; + self.locationManager = [CLLocationManager new]; + self.locationManager.delegate = self; + if (always) { + [self.locationManager requestAlwaysAuthorization]; + } else { + [self.locationManager requestWhenInUseAuthorization]; + } +} + +- (void)locationManager:(CLLocationManager *)manager + didChangeAuthorizationStatus:(CLAuthorizationStatus)status { + if (status == kCLAuthorizationStatusNotDetermined) { + return; + } + if (self.completion) { + self.completion(status); + self.completion = nil; + } +} + +@end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h index 212cb2122411..92d843793990 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h @@ -1,3 +1,7 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + #import @interface WifiInfoFlutterPlugin : NSObject diff --git a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m index d4577610e3db..3abdbeef5744 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m @@ -1,20 +1,132 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + #import "WifiInfoFlutterPlugin.h" +#import +#import "FLTWifiInfoLocationHandler.h" +#import "SystemConfiguration/CaptiveNetwork.h" + +#include + +#include + +@interface WifiInfoFlutterPlugin () +@property(strong, nonatomic) FLTWifiInfoLocationHandler* locationHandler; +@end + @implementation WifiInfoFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { + WifiInfoFlutterPlugin* instance = [[WifiInfoFlutterPlugin alloc] init]; + FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"wifi_info_flutter" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/wifi_info_flutter" binaryMessenger:[registrar messenger]]; - WifiInfoFlutterPlugin* instance = [[WifiInfoFlutterPlugin alloc] init]; [registrar addMethodCallDelegate:instance channel:channel]; } +- (NSString*)findNetworkInfo:(NSString*)key { + NSString* info = nil; + NSArray* interfaceNames = (__bridge_transfer id)CNCopySupportedInterfaces(); + for (NSString* interfaceName in interfaceNames) { + NSDictionary* networkInfo = + (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName); + if (networkInfo[key]) { + info = networkInfo[key]; + } + } + return info; +} + +- (NSString*)getWifiName { + return [self findNetworkInfo:@"SSID"]; +} + +- (NSString*)getBSSID { + return [self findNetworkInfo:@"BSSID"]; +} + +- (NSString*)getWifiIP { + NSString* address = @"error"; + struct ifaddrs* interfaces = NULL; + struct ifaddrs* temp_addr = NULL; + int success = 0; + + // retrieve the current interfaces - returns 0 on success + success = getifaddrs(&interfaces); + if (success == 0) { + // Loop through linked list of interfaces + temp_addr = interfaces; + while (temp_addr != NULL) { + if (temp_addr->ifa_addr->sa_family == AF_INET) { + // Check if interface is en0 which is the wifi connection on the iPhone + if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) { + // Get NSString from C String + address = [NSString + stringWithUTF8String:inet_ntoa(((struct sockaddr_in*)temp_addr->ifa_addr)->sin_addr)]; + } + } + + temp_addr = temp_addr->ifa_next; + } + } + + // Free memory + freeifaddrs(interfaces); + + return address; +} + - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - if ([@"getPlatformVersion" isEqualToString:call.method]) { - result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + if ([call.method isEqualToString:@"wifiName"]) { + result([self getWifiName]); + } else if ([call.method isEqualToString:@"wifiBSSID"]) { + result([self getBSSID]); + } else if ([call.method isEqualToString:@"wifiIPAddress"]) { + result([self getWifiIP]); + } else if ([call.method isEqualToString:@"getLocationServiceAuthorization"]) { + result([self convertCLAuthorizationStatusToString:[FLTWifiInfoLocationHandler + locationAuthorizationStatus]]); + } else if ([call.method isEqualToString:@"requestLocationServiceAuthorization"]) { + NSArray* arguments = call.arguments; + BOOL always = [arguments.firstObject boolValue]; + __weak typeof(self) weakSelf = self; + [self.locationHandler + requestLocationAuthorization:always + completion:^(CLAuthorizationStatus status) { + result([weakSelf convertCLAuthorizationStatusToString:status]); + }]; } else { result(FlutterMethodNotImplemented); } } +- (NSString*)convertCLAuthorizationStatusToString:(CLAuthorizationStatus)status { + switch (status) { + case kCLAuthorizationStatusNotDetermined: { + return @"notDetermined"; + } + case kCLAuthorizationStatusRestricted: { + return @"restricted"; + } + case kCLAuthorizationStatusDenied: { + return @"denied"; + } + case kCLAuthorizationStatusAuthorizedAlways: { + return @"authorizedAlways"; + } + case kCLAuthorizationStatusAuthorizedWhenInUse: { + return @"authorizedWhenInUse"; + } + default: { return @"unknown"; } + } +} + +- (FLTWifiInfoLocationHandler*)locationHandler { + if (!_locationHandler) { + _locationHandler = [FLTWifiInfoLocationHandler new]; + } + return _locationHandler; +} @end diff --git a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart index 5711e4be147d..1183bf6c74bf 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart @@ -1,15 +1,153 @@ -// ignore_for_file: public_member_api_docs +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:async'; import 'package:flutter/services.dart'; +import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; -class WifiInfoFlutter { - static const MethodChannel _channel = - const MethodChannel('wifi_info_flutter'); +// Export enums from the platform_interface so plugin users can use them directly. +export 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart' + show LocationAuthorizationStatus; - static Future get platformVersion async { - final String version = await _channel.invokeMethod('getPlatformVersion'); - return version; +/// Checks WI-FI status and more. +class WifiInfo { + /// Constructs a singleton instance of [WifiInfo]. + /// + /// [WifiInfo] is designed to work as a singleton. + factory WifiInfo() { + if (_singleton == null) { + _singleton = WifiInfo._(); + } + return _singleton; + } + + WifiInfo._(); + + static WifiInfo _singleton; + + static WifiInfoFlutterPlatform get _platform => + WifiInfoFlutterPlatform.instance; + + /// Obtains the wifi name (SSID) of the connected network + /// + /// Please note that it DOESN'T WORK on emulators (returns null). + /// + /// From android 8.0 onwards the GPS must be ON (high accuracy) + /// in order to be able to obtain the SSID. + Future getWifiName() { + return _platform.getWifiName(); + } + + /// Obtains the wifi BSSID of the connected network. + /// + /// Please note that it DOESN'T WORK on emulators (returns null). + /// + /// From Android 8.0 onwards the GPS must be ON (high accuracy) + /// in order to be able to obtain the BSSID. + Future getWifiBSSID() { + return _platform.getWifiBSSID(); + } + + /// Obtains the IP address of the connected wifi network + Future getWifiIP() { + return _platform.getWifiIP(); + } + + /// Request to authorize the location service (Only on iOS). + /// + /// This method will throw a [PlatformException] on Android. + /// + /// Returns a [LocationAuthorizationStatus] after user authorized or denied the location on this request. + /// + /// If the location information needs to be accessible all the time, set `requestAlwaysLocationUsage` to true. If user has + /// already granted a [LocationAuthorizationStatus.authorizedWhenInUse] prior to requesting an "always" access, it will return [LocationAuthorizationStatus.denied]. + /// + /// If the location service authorization is not determined prior to making this call, a platform standard UI of requesting a location service will pop up. + /// This UI will only show once unless the user re-install the app to their phone which resets the location service authorization to not determined. + /// + /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. + /// It can be replaced with other permission handling code/plugin if preferred. + /// To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + /// * `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information + /// all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. + /// * `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is + /// running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. + /// + /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: + /// + /// * The app uses Core Location, and has the user’s authorization to use location information. + /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + /// * The app has active VPN configurations installed. + /// + /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. + /// For example, + /// ```dart + /// if (Platform.isIOS) { + /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); + /// if (status == LocationAuthorizationStatus.notDetermined) { + /// status = await _connectivity.requestLocationServiceAuthorization(); + /// } + /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } else { + /// print('location service is not authorized, the data might not be correct'); + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// } else { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// ``` + /// + /// Ideally, a location service authorization should only be requested if the current authorization status is not determined. + /// + /// See also [getLocationServiceAuthorization] to obtain current location service status. + Future requestLocationServiceAuthorization({ + bool requestAlwaysLocationUsage = false, + }) { + return _platform.requestLocationServiceAuthorization( + requestAlwaysLocationUsage: requestAlwaysLocationUsage, + ); + } + + /// Get the current location service authorization (Only on iOS). + /// + /// This method will throw a [PlatformException] on Android. + /// + /// Returns a [LocationAuthorizationStatus]. + /// If the returned value is [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] call + /// can request the authorization. + /// If the returned value is not [LocationAuthorizationStatus.notDetermined], a subsequent [requestLocationServiceAuthorization] + /// will not initiate another request. It will instead return the "determined" status. + /// + /// This method is a helper to get the location authorization that is necessary for certain functionality of this plugin. + /// It can be replaced with other permission handling code/plugin if preferred. + /// + /// Starting from iOS 13, `getWifiBSSID` and `getWifiIP` will only work properly if: + /// + /// * The app uses Core Location, and has the user’s authorization to use location information. + /// * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + /// * The app has active VPN configurations installed. + /// + /// If the app falls into the first category, call this method before calling `getWifiBSSID` or `getWifiIP`. + /// For example, + /// ```dart + /// if (Platform.isIOS) { + /// LocationAuthorizationStatus status = await _connectivity.getLocationServiceAuthorization(); + /// if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } else { + /// print('location service is not authorized, the data might not be correct'); + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// } else { + /// wifiBSSID = await _connectivity.getWifiName(); + /// } + /// ``` + /// + /// See also [requestLocationServiceAuthorization] for requesting a location service authorization. + Future getLocationServiceAuthorization() { + return _platform.getLocationServiceAuthorization(); } } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml index 6a7276b97f33..4d2ca546e2d5 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -2,6 +2,7 @@ name: wifi_info_flutter description: A new flutter plugin project. version: 0.0.1 homepage: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter +publish_to: none environment: sdk: ">=2.7.0 <3.0.0" @@ -10,20 +11,18 @@ environment: dependencies: flutter: sdk: flutter + wifi_info_flutter_platform_interface: + path: ../wifi_info_flutter_platform_interface dev_dependencies: + mockito: ^4.1.1 + plugin_platform_interface: ^1.0.0 + integration_test: + path: ../../integration_test flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' and Android 'package' identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: android: @@ -31,34 +30,3 @@ flutter: pluginClass: WifiInfoFlutterPlugin ios: pluginClass: WifiInfoFlutterPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart index 93256726db43..a3a55170bce5 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart @@ -1,23 +1,85 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.8 + import 'package:wifi_info_flutter/wifi_info_flutter.dart'; +import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:mockito/mockito.dart'; + +const String kWifiNameResult = '1337wifi'; +const String kWifiBSSIDResult = 'c0:ff:33:c0:d3:55'; +const String kWifiIpAddressResult = '127.0.0.1'; +const LocationAuthorizationStatus kRequestLocationResult = + LocationAuthorizationStatus.authorizedAlways; +const LocationAuthorizationStatus kGetLocationResult = + LocationAuthorizationStatus.authorizedAlways; void main() { - const MethodChannel channel = MethodChannel('wifi_info_flutter'); + group('$WifiInfo', () { + WifiInfo wifiInfo; + MockWifiInfoFlutterPlatform fakePlatform; - TestWidgetsFlutterBinding.ensureInitialized(); + setUp(() async { + fakePlatform = MockWifiInfoFlutterPlatform(); + WifiInfoFlutterPlatform.instance = fakePlatform; + wifiInfo = WifiInfo(); + }); - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; + test('getWifiName', () async { + String result = await wifiInfo.getWifiName(); + expect(result, kWifiNameResult); }); - }); - tearDown(() { - channel.setMockMethodCallHandler(null); - }); + test('getWifiBSSID', () async { + String result = await wifiInfo.getWifiBSSID(); + expect(result, kWifiBSSIDResult); + }); + + test('getWifiIP', () async { + String result = await wifiInfo.getWifiIP(); + expect(result, kWifiIpAddressResult); + }); + + test('requestLocationServiceAuthorization', () async { + LocationAuthorizationStatus result = + await wifiInfo.requestLocationServiceAuthorization(); + expect(result, kRequestLocationResult); + }); - test('getPlatformVersion', () async { - expect(await WifiInfoFlutter.platformVersion, '42'); + test('getLocationServiceAuthorization', () async { + LocationAuthorizationStatus result = + await wifiInfo.getLocationServiceAuthorization(); + expect(result, kRequestLocationResult); + }); }); } + +class MockWifiInfoFlutterPlatform extends Mock + with MockPlatformInterfaceMixin + implements WifiInfoFlutterPlatform { + Future getWifiName() async { + return kWifiNameResult; + } + + Future getWifiBSSID() async { + return kWifiBSSIDResult; + } + + Future getWifiIP() async { + return kWifiIpAddressResult; + } + + Future requestLocationServiceAuthorization({ + bool requestAlwaysLocationUsage = false, + }) async { + return kRequestLocationResult; + } + + Future getLocationServiceAuthorization() async { + return kGetLocationResult; + } +}