diff --git a/packages/android_local_network/CHANGELOG.md b/packages/android_local_network/CHANGELOG.md new file mode 100644 index 000000000000..bca56a27a32d --- /dev/null +++ b/packages/android_local_network/CHANGELOG.md @@ -0,0 +1,10 @@ +## 0.1.1 + +* `AndroidLocalAreaSocket.connect` now automatically requests permission on first use. +* Synchronized `AndroidLocalNetwork.requestPermission` to handle concurrent calls. + +## 0.1.0 + +* Initial release. +* Added `AndroidLocalNetwork` to check and request `ACCESS_LOCAL_NETWORK` permission. +* Added `AndroidLocalAreaSocket` wrapper for `Socket.connect`. diff --git a/packages/android_local_network/README.md b/packages/android_local_network/README.md new file mode 100644 index 000000000000..9cd4d9f93a9f --- /dev/null +++ b/packages/android_local_network/README.md @@ -0,0 +1,27 @@ +# android_local_network + +A Flutter package to handle the Android 17 Local Area Permission (`ACCESS_LOCAL_NETWORK`) for Dart sockets. + +## Usage + +Instead of using `Socket.connect` directly on Android 17+, use `AndroidLocalAreaSocket.connect`: + +```dart +import 'package:android_local_network/android_local_network.dart'; + +final socket = await AndroidLocalAreaSocket.connect('192.168.1.1', 8080); +``` + +Or check and request the permission manually: + +```dart +import 'package:android_local_network/android_local_network.dart'; + +if (await AndroidLocalNetwork.requestPermission()) { + // Permission granted +} +``` + +## Implementation Details + +This package uses `jnigen` to interact with Android's permission system via FFI. This avoids the need for MethodChannels in the framework and provides a more direct way to handle permissions from Dart. diff --git a/packages/android_local_network/android/build.gradle.kts b/packages/android_local_network/android/build.gradle.kts new file mode 100644 index 000000000000..ed4a2e121845 --- /dev/null +++ b/packages/android_local_network/android/build.gradle.kts @@ -0,0 +1,25 @@ +group = "com.example.android_local_network" +version = "1.0-SNAPSHOT" + +plugins { + id("com.android.library") +} + +android { + namespace = "com.example.android_local_network" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation("androidx.core:core:1.13.1") + implementation("io.flutter:flutter_embedding_debug:1.0.0-cafac705f02f2a77cd72743c41b33a0fa97714e0") +} diff --git a/packages/android_local_network/android/src/main/java/com/example/android_local_network/AndroidLocalNetworkPlugin.java b/packages/android_local_network/android/src/main/java/com/example/android_local_network/AndroidLocalNetworkPlugin.java new file mode 100644 index 000000000000..46a278298663 --- /dev/null +++ b/packages/android_local_network/android/src/main/java/com/example/android_local_network/AndroidLocalNetworkPlugin.java @@ -0,0 +1,136 @@ +package com.example.android_local_network; + +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.PluginRegistry; + +import android.os.Build; +import android.util.Log; + +/** + * AndroidLocalNetwork handles the ACCESS_LOCAL_NETWORK permission. + * It can be used via jnigen to check and request permissions from Dart. + */ +public class AndroidLocalNetworkPlugin implements FlutterPlugin, ActivityAware, PluginRegistry.RequestPermissionsResultListener { + private static final String TAG = "AndroidLocalNetwork"; + private static final String PERMISSION = "android.permission.ACCESS_LOCAL_NETWORK"; + private static final int REQUEST_CODE = 4853; // Arbitrary request code + + private Activity activity; + private PermissionCallback pendingCallback; + + /** + * Interface for permission result callbacks. + */ + public interface PermissionCallback { + void onResult(boolean granted); + } + + private static AndroidLocalNetworkPlugin instance; + + public static AndroidLocalNetworkPlugin getInstance() { + Log.d(TAG, "getInstance called, instance is " + (instance == null ? "null" : "not null")); + return instance; + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + Log.d(TAG, "onAttachedToEngine"); + instance = this; + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + Log.d(TAG, "onDetachedFromEngine"); + if (instance == this) { + instance = null; + } + } + + @Override + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { + Log.d(TAG, "onAttachedToActivity"); + this.activity = binding.getActivity(); + binding.addRequestPermissionsResultListener(this); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + Log.d(TAG, "onDetachedFromActivityForConfigChanges"); + this.activity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { + Log.d(TAG, "onReattachedToActivityForConfigChanges"); + this.activity = binding.getActivity(); + binding.addRequestPermissionsResultListener(this); + } + + @Override + public void onDetachedFromActivity() { + Log.d(TAG, "onDetachedFromActivity"); + this.activity = null; + } + + /** + * Checks if the local area permission is granted. + */ + public boolean checkPermission() { + if (activity == null) { + Log.w(TAG, "checkPermission: activity is null"); + return false; + } + try { + boolean granted = ContextCompat.checkSelfPermission(activity, PERMISSION) == PackageManager.PERMISSION_GRANTED; + Log.d(TAG, "checkPermission for " + PERMISSION + ": " + granted); + return granted; + } catch (Exception e) { + Log.e(TAG, "checkPermission error", e); + return false; + } + } + + /** + * Requests the local area permission. + */ + public void requestPermission(PermissionCallback callback) { + Log.d(TAG, "requestPermission called. Current API: " + Build.VERSION.SDK_INT); + + if (activity == null) { + Log.w(TAG, "requestPermission: activity is null"); + callback.onResult(false); + return; + } + + if (checkPermission()) { + callback.onResult(true); + return; + } + + pendingCallback = callback; + Log.d(TAG, "requestPermission: requesting from OS: " + PERMISSION); + ActivityCompat.requestPermissions(activity, new String[]{PERMISSION}, REQUEST_CODE); + } + + @Override + public boolean onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Log.d(TAG, "onRequestPermissionsResult: requestCode=" + requestCode); + if (requestCode == REQUEST_CODE) { + boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; + Log.d(TAG, "onRequestPermissionsResult: granted=" + granted); + if (pendingCallback != null) { + pendingCallback.onResult(granted); + pendingCallback = null; + } + return true; + } + return false; + } +} diff --git a/packages/android_local_network/example/.gitignore b/packages/android_local_network/example/.gitignore new file mode 100644 index 000000000000..3820a95c65c3 --- /dev/null +++ b/packages/android_local_network/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/android_local_network/example/.metadata b/packages/android_local_network/example/.metadata new file mode 100644 index 000000000000..bfb685dbd862 --- /dev/null +++ b/packages/android_local_network/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "aa55a185405913779b327aa3d6c6639d2487642f" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: aa55a185405913779b327aa3d6c6639d2487642f + base_revision: aa55a185405913779b327aa3d6c6639d2487642f + - platform: android + create_revision: aa55a185405913779b327aa3d6c6639d2487642f + base_revision: aa55a185405913779b327aa3d6c6639d2487642f + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/android_local_network/example/README.md b/packages/android_local_network/example/README.md new file mode 100644 index 000000000000..83f7e49e4b72 --- /dev/null +++ b/packages/android_local_network/example/README.md @@ -0,0 +1,17 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/android_local_network/example/analysis_options.yaml b/packages/android_local_network/example/analysis_options.yaml new file mode 100644 index 000000000000..0d2902135cae --- /dev/null +++ b/packages/android_local_network/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/android_local_network/example/android/.gitignore b/packages/android_local_network/example/android/.gitignore new file mode 100644 index 000000000000..be3943c96d8e --- /dev/null +++ b/packages/android_local_network/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packages/android_local_network/example/android/app/build.gradle.kts b/packages/android_local_network/example/android/app/build.gradle.kts new file mode 100644 index 000000000000..8a3ee8e9014d --- /dev/null +++ b/packages/android_local_network/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.example" + compileSdk = 36 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 24 + targetSdk = 36 + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/android_local_network/example/android/app/src/debug/AndroidManifest.xml b/packages/android_local_network/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/android_local_network/example/android/app/src/main/AndroidManifest.xml b/packages/android_local_network/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..fcf7404d17e6 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/android_local_network/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/android_local_network/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 000000000000..ac81bae644ac --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packages/android_local_network/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/android_local_network/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/android_local_network/example/android/app/src/main/res/drawable/launch_background.xml b/packages/android_local_network/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/android_local_network/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/android_local_network/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000000..db77bb4b7b09 Binary files /dev/null and b/packages/android_local_network/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/android_local_network/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/android_local_network/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/android_local_network/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/android_local_network/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/android_local_network/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000000..09d4391482be Binary files /dev/null and b/packages/android_local_network/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/android_local_network/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/android_local_network/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000000..d5f1c8d34e7a Binary files /dev/null and b/packages/android_local_network/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/android_local_network/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/android_local_network/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000000..4d6372eebdb2 Binary files /dev/null and b/packages/android_local_network/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/android_local_network/example/android/app/src/main/res/values-night/styles.xml b/packages/android_local_network/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..06952be745f9 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/android_local_network/example/android/app/src/main/res/values/styles.xml b/packages/android_local_network/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..cb1ef88056ed --- /dev/null +++ b/packages/android_local_network/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/android_local_network/example/android/app/src/profile/AndroidManifest.xml b/packages/android_local_network/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..399f6981d5d3 --- /dev/null +++ b/packages/android_local_network/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/android_local_network/example/android/build.gradle.kts b/packages/android_local_network/example/android/build.gradle.kts new file mode 100644 index 000000000000..dbee657bb5b9 --- /dev/null +++ b/packages/android_local_network/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/android_local_network/example/android/gradle.properties b/packages/android_local_network/example/android/gradle.properties new file mode 100644 index 000000000000..d5da7278a400 --- /dev/null +++ b/packages/android_local_network/example/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/packages/android_local_network/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/android_local_network/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..e4ef43fb98df --- /dev/null +++ b/packages/android_local_network/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/android_local_network/example/android/settings.gradle.kts b/packages/android_local_network/example/android/settings.gradle.kts new file mode 100644 index 000000000000..ca7fe065c167 --- /dev/null +++ b/packages/android_local_network/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/packages/android_local_network/example/lib/main.dart b/packages/android_local_network/example/lib/main.dart new file mode 100644 index 000000000000..1ed196a37bdc --- /dev/null +++ b/packages/android_local_network/example/lib/main.dart @@ -0,0 +1,241 @@ +// Copyright 2013 The Flutter Authors +// 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:android_local_network/android_local_network.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Android Local Network Example', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + useMaterial3: true, + ), + home: const MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + String _status = 'Unknown'; + String _lastAction = 'None'; + bool _isScanning = false; + final List _foundDevices = []; + final List _interfacesInfo = []; + + @override + void initState() { + super.initState(); + _refreshInterfaces(); + } + + Future _refreshInterfaces() async { + final interfaces = await NetworkInterface.list(); + setState(() { + _interfacesInfo.clear(); + for (final interface in interfaces) { + for (final addr in interface.addresses) { + if (addr.type == InternetAddressType.IPv4) { + _interfacesInfo.add('${interface.name}: ${addr.address}'); + } + } + } + }); + } + + Future _checkPermission() async { + final bool granted = await AndroidLocalNetwork.checkPermission(); + setState(() { + _status = granted ? 'Granted' : 'Denied/Not Requested'; + _lastAction = 'Checked Permission'; + }); + } + + Future _scanLan() async { + setState(() { + _isScanning = true; + _foundDevices.clear(); + _lastAction = 'Scanning all IPv4 interfaces (ports 80, 8080, 443)...'; + }); + + try { + final interfaces = await NetworkInterface.list(); + final List subnets = []; + for (final interface in interfaces) { + for (final addr in interface.addresses) { + if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) { + final parts = addr.address.split('.'); + subnets.add('${parts[0]}.${parts[1]}.${parts[2]}'); + } + } + } + + if (subnets.isEmpty) { + setState(() { + _lastAction = 'No non-loopback IPv4 interfaces found'; + _isScanning = false; + }); + return; + } + + final List> scans = []; + final List ports = [80, 8080, 443, 22]; + + for (final subnet in subnets) { + for (int i = 1; i <= 254; i++) { + final ip = '$subnet.$i'; + for (final port in ports) { + scans.add(() async { + try { + final socket = await AndroidLocalAreaSocket.connect( + ip, + port, + timeout: const Duration(milliseconds: 500), + ); + await socket.close(); + setState(() { + final entry = '$ip:$port'; + if (!_foundDevices.contains(entry)) { + _foundDevices.add(entry); + } + }); + } catch (_) { + // Ignore connection failures + } + }()); + } + } + } + + await Future.wait(scans); + setState(() { + _lastAction = 'Scan complete. Found ${_foundDevices.length} devices.'; + }); + } catch (e) { + setState(() { + _lastAction = 'Scan error: $e'; + }); + } finally { + setState(() { + _isScanning = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Android Local Network'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: _refreshInterfaces, + tooltip: 'Refresh Interfaces', + ), + ], + ), + body: Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Permission Status:', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + _status, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: _status == 'Granted' ? Colors.green : Colors.red, + ), + ), + const SizedBox(height: 20), + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + const Text('Detected Interfaces:', + style: TextStyle(fontWeight: FontWeight.bold)), + ..._interfacesInfo.map((info) => Text(info)), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + 'Last Action:', + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + _lastAction, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 30), + Wrap( + spacing: 10, + runSpacing: 10, + alignment: WrapAlignment.center, + children: [ + ElevatedButton( + onPressed: _checkPermission, + child: const Text('Check Status'), + ), + ElevatedButton( + onPressed: _isScanning ? null : _scanLan, + child: _isScanning + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Deep Scan LAN'), + ), + ], + ), + if (_foundDevices.isNotEmpty) ...[ + const SizedBox(height: 20), + const Divider(), + const Text('Found Devices:', + style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 10), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _foundDevices.length, + itemBuilder: (context, index) { + return Center(child: Text(_foundDevices[index])); + }, + ), + ], + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/android_local_network/example/pubspec.yaml b/packages/android_local_network/example/pubspec.yaml new file mode 100644 index 000000000000..fddebfd060e9 --- /dev/null +++ b/packages/android_local_network/example/pubspec.yaml @@ -0,0 +1,91 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.13.0-68.0.dev + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + android_local_network: + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# 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 packages. +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/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # 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/to/font-from-package diff --git a/packages/android_local_network/example/test/widget_test.dart b/packages/android_local_network/example/test/widget_test.dart new file mode 100644 index 000000000000..00e84f8492a0 --- /dev/null +++ b/packages/android_local_network/example/test/widget_test.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:example/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Android Local Network example smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that the initial status is 'Unknown'. + expect(find.text('Unknown'), findsOneWidget); + + // Verify that the 'Deep Scan LAN' button is present. + expect(find.text('Deep Scan LAN'), findsOneWidget); + + // Verify that the 'Check Status' button is present. + expect(find.text('Check Status'), findsOneWidget); + + // Verify that 'Request Permission' is NOT present. + expect(find.text('Request Permission'), findsNothing); + }); +} diff --git a/packages/android_local_network/lib/android_local_network.dart b/packages/android_local_network/lib/android_local_network.dart new file mode 100644 index 000000000000..a5ee92a0f938 --- /dev/null +++ b/packages/android_local_network/lib/android_local_network.dart @@ -0,0 +1,137 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'src/android_local_network.g.dart'; + +/// Provides access to Android Local Area Network permission. +class AndroidLocalNetwork { + static Future? _pendingRequest; + static bool _hasRequestedOnce = false; + + /// Checks if the local area permission is granted. + /// Returns true on non-Android platforms. + static Future checkPermission() async { + if (!Platform.isAndroid) { + return true; + } + final AndroidLocalNetworkPlugin? plugin = AndroidLocalNetworkPlugin.instance; + if (plugin == null) { + return false; + } + final bool result = plugin.checkPermission(); + plugin.release(); + return result; + } + + /// Requests the local area permission. + /// Returns true if granted, false otherwise. + /// Returns true on non-Android platforms. + /// + /// Multiple concurrent calls will wait for the same request to complete. + static Future requestPermission() async { + if (!Platform.isAndroid) { + return true; + } + + if (_pendingRequest != null) { + return _pendingRequest!; + } + + final completer = Completer(); + _pendingRequest = completer.future; + _hasRequestedOnce = true; + + try { + final AndroidLocalNetworkPlugin? plugin = AndroidLocalNetworkPlugin.instance; + if (plugin == null) { + print('AndroidLocalNetwork: plugin instance is null'); + completer.complete(false); + return false; + } + + // Check if already granted to avoid unnecessary callback setup. + if (plugin.checkPermission()) { + plugin.release(); + completer.complete(true); + return true; + } + + final callback = AndroidLocalNetworkPlugin$PermissionCallback.implement( + $AndroidLocalNetworkPlugin$PermissionCallback( + onResult: (bool granted) { + print('AndroidLocalNetwork: Dart callback received result: $granted'); + if (!completer.isCompleted) { + completer.complete(granted); + } + }, + ), + ); + + print('AndroidLocalNetwork: Requesting permission via plugin...'); + plugin.requestPermission(callback); + final bool result = await completer.future; + print('AndroidLocalNetwork: Request result: $result'); + + callback.release(); + plugin.release(); + return result; + } catch (e) { + print('AndroidLocalNetwork: Error requesting permission: $e'); + if (!completer.isCompleted) { + completer.complete(false); + } + return false; + } finally { + _pendingRequest = null; + } + } + + /// Internal method to check and potentially request permission automatically. + static Future _checkAndRequestPermission() async { + if (await checkPermission()) { + return true; + } + // If we haven't requested yet, or if a request is already in progress, + // call requestPermission (which handles synchronization). + if (!_hasRequestedOnce || _pendingRequest != null) { + return requestPermission(); + } + return false; + } +} + +/// A wrapper around [Socket] that ensures the local area permission is granted on Android. +class AndroidLocalAreaSocket { + /// Connects to a socket, requesting permission first on Android if necessary. + /// + /// On Android, it will automatically request the ACCESS_LOCAL_NETWORK + /// permission on the first call to [connect] if it hasn't been granted. + /// Subsequent calls will only check if the permission is currently granted. + static Future connect( + Object host, + int port, { + Object? sourceAddress, + int sourcePort = 0, + Duration? timeout, + }) async { + if (Platform.isAndroid) { + final bool granted = await AndroidLocalNetwork._checkAndRequestPermission(); + if (!granted) { + throw const SocketException( + 'ACCESS_LOCAL_NETWORK permission denied', + ); + } + } + return Socket.connect( + host, + port, + sourceAddress: sourceAddress, + sourcePort: sourcePort, + timeout: timeout, + ); + } +} diff --git a/packages/android_local_network/lib/src/android_local_network.g.dart b/packages/android_local_network/lib/src/android_local_network.g.dart new file mode 100644 index 000000000000..c615c289368c --- /dev/null +++ b/packages/android_local_network/lib/src/android_local_network.g.dart @@ -0,0 +1,589 @@ +// AUTO GENERATED BY JNIGEN 0.16.0. DO NOT EDIT! + +// ignore_for_file: annotate_overrides +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: camel_case_extensions +// ignore_for_file: camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: comment_references +// ignore_for_file: doc_directive_unknown +// ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter +// ignore_for_file: invalid_internal_annotation +// ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: library_prefixes +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_local_identifiers +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: only_throw_errors +// ignore_for_file: overridden_fields +// ignore_for_file: prefer_double_quotes +// ignore_for_file: unintended_html_in_doc_comment +// ignore_for_file: unnecessary_cast +// ignore_for_file: unnecessary_non_null_assertion +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: unused_element +// ignore_for_file: unused_field +// ignore_for_file: unused_import +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_shown_name +// ignore_for_file: use_super_parameters + +import 'dart:core' as core$_; +import 'dart:core' show Object, String; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + +const _$jniVersionCheck = jni$_.JniVersionCheck(1, 0); + +/// from: `com.example.android_local_network.AndroidLocalNetworkPlugin` +/// +/// AndroidLocalNetwork handles the ACCESS_LOCAL_NETWORK permission. +/// It can be used via jnigen to check and request permissions from Dart. +extension type AndroidLocalNetworkPlugin._(jni$_.JObject _$this) + implements jni$_.JObject { + static final _class = jni$_.JClass.forName( + r'com/example/android_local_network/AndroidLocalNetworkPlugin', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $AndroidLocalNetworkPlugin$Type$(); + static final _id_new$ = _class.constructorId(r'()V'); + + static final _new$ = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_NewObject') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void ()` + /// The returned object must be released after use, by calling the [release] method. + factory AndroidLocalNetworkPlugin() { + return _new$( + _class.reference.pointer, + _id_new$.pointer, + ).object(); + } + + static final _id_get$instance = _class.staticMethodId( + r'getInstance', + r'()Lcom/example/android_local_network/AndroidLocalNetworkPlugin;', + ); + + static final _get$instance = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public com.example.android_local_network.AndroidLocalNetworkPlugin getInstance()` + /// The returned object must be released after use, by calling the [release] method. + static AndroidLocalNetworkPlugin? get instance { + return _get$instance( + _class.reference.pointer, + _id_get$instance.pointer, + ).object(); + } +} + +extension AndroidLocalNetworkPlugin$$Methods on AndroidLocalNetworkPlugin { + static final _id_onAttachedToEngine = AndroidLocalNetworkPlugin._class + .instanceMethodId( + r'onAttachedToEngine', + r'(Lio/flutter/embedding/engine/plugins/FlutterPlugin$FlutterPluginBinding;)V', + ); + + static final _onAttachedToEngine = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onAttachedToEngine(io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding binding)` + void onAttachedToEngine(jni$_.JObject binding) { + final _$binding = binding.reference; + _onAttachedToEngine( + reference.pointer, + _id_onAttachedToEngine.pointer, + _$binding.pointer, + ).check(); + } + + static final _id_onDetachedFromEngine = AndroidLocalNetworkPlugin._class + .instanceMethodId( + r'onDetachedFromEngine', + r'(Lio/flutter/embedding/engine/plugins/FlutterPlugin$FlutterPluginBinding;)V', + ); + + static final _onDetachedFromEngine = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onDetachedFromEngine(io.flutter.embedding.engine.plugins.FlutterPlugin$FlutterPluginBinding binding)` + void onDetachedFromEngine(jni$_.JObject binding) { + final _$binding = binding.reference; + _onDetachedFromEngine( + reference.pointer, + _id_onDetachedFromEngine.pointer, + _$binding.pointer, + ).check(); + } + + static final _id_onAttachedToActivity = AndroidLocalNetworkPlugin._class + .instanceMethodId( + r'onAttachedToActivity', + r'(Lio/flutter/embedding/engine/plugins/activity/ActivityPluginBinding;)V', + ); + + static final _onAttachedToActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onAttachedToActivity(io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding binding)` + void onAttachedToActivity(jni$_.JObject binding) { + final _$binding = binding.reference; + _onAttachedToActivity( + reference.pointer, + _id_onAttachedToActivity.pointer, + _$binding.pointer, + ).check(); + } + + static final _id_onDetachedFromActivityForConfigChanges = + AndroidLocalNetworkPlugin._class.instanceMethodId( + r'onDetachedFromActivityForConfigChanges', + r'()V', + ); + + static final _onDetachedFromActivityForConfigChanges = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onDetachedFromActivityForConfigChanges()` + void onDetachedFromActivityForConfigChanges() { + _onDetachedFromActivityForConfigChanges( + reference.pointer, + _id_onDetachedFromActivityForConfigChanges.pointer, + ).check(); + } + + static final _id_onReattachedToActivityForConfigChanges = + AndroidLocalNetworkPlugin._class.instanceMethodId( + r'onReattachedToActivityForConfigChanges', + r'(Lio/flutter/embedding/engine/plugins/activity/ActivityPluginBinding;)V', + ); + + static final _onReattachedToActivityForConfigChanges = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void onReattachedToActivityForConfigChanges(io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding binding)` + void onReattachedToActivityForConfigChanges(jni$_.JObject binding) { + final _$binding = binding.reference; + _onReattachedToActivityForConfigChanges( + reference.pointer, + _id_onReattachedToActivityForConfigChanges.pointer, + _$binding.pointer, + ).check(); + } + + static final _id_onDetachedFromActivity = AndroidLocalNetworkPlugin._class + .instanceMethodId(r'onDetachedFromActivity', r'()V'); + + static final _onDetachedFromActivity = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public void onDetachedFromActivity()` + void onDetachedFromActivity() { + _onDetachedFromActivity( + reference.pointer, + _id_onDetachedFromActivity.pointer, + ).check(); + } + + static final _id_checkPermission = AndroidLocalNetworkPlugin._class + .instanceMethodId(r'checkPermission', r'()Z'); + + static final _checkPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public boolean checkPermission()` + /// + /// Checks if the local area permission is granted. + core$_.bool checkPermission() { + return _checkPermission( + reference.pointer, + _id_checkPermission.pointer, + ).boolean; + } + + static final _id_requestPermission = AndroidLocalNetworkPlugin._class + .instanceMethodId( + r'requestPermission', + r'(Lcom/example/android_local_network/AndroidLocalNetworkPlugin$PermissionCallback;)V', + ); + + static final _requestPermission = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Pointer,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + ) + >(); + + /// from: `public void requestPermission(com.example.android_local_network.AndroidLocalNetworkPlugin$PermissionCallback callback)` + /// + /// Requests the local area permission. + void requestPermission( + AndroidLocalNetworkPlugin$PermissionCallback? callback, + ) { + final _$callback = callback?.reference ?? jni$_.jNullReference; + _requestPermission( + reference.pointer, + _id_requestPermission.pointer, + _$callback.pointer, + ).check(); + } + + static final _id_onRequestPermissionsResult = AndroidLocalNetworkPlugin._class + .instanceMethodId( + r'onRequestPermissionsResult', + r'(I[Ljava/lang/String;[I)Z', + ); + + static final _onRequestPermissionsResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `public boolean onRequestPermissionsResult(int requestCode, java.lang.String[] permissions, int[] grantResults)` + core$_.bool onRequestPermissionsResult( + core$_.int requestCode, + jni$_.JArray permissions, + jni$_.JIntArray grantResults, + ) { + final _$permissions = permissions.reference; + final _$grantResults = grantResults.reference; + return _onRequestPermissionsResult( + reference.pointer, + _id_onRequestPermissionsResult.pointer, + requestCode, + _$permissions.pointer, + _$grantResults.pointer, + ).boolean; + } +} + +final class $AndroidLocalNetworkPlugin$Type$ + extends jni$_.JType { + @jni$_.internal + const $AndroidLocalNetworkPlugin$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/example/android_local_network/AndroidLocalNetworkPlugin;'; +} + +/// from: `com.example.android_local_network.AndroidLocalNetworkPlugin$PermissionCallback` +/// +/// Interface for permission result callbacks. +extension type AndroidLocalNetworkPlugin$PermissionCallback._( + jni$_.JObject _$this +) implements jni$_.JObject { + static final _class = jni$_.JClass.forName( + r'com/example/android_local_network/AndroidLocalNetworkPlugin$PermissionCallback', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $AndroidLocalNetworkPlugin$PermissionCallback$Type$(); + + /// Maps a specific port to the implemented interface. + static final core$_.Map< + core$_.int, + $AndroidLocalNetworkPlugin$PermissionCallback + > + _$impls = {}; + static jni$_.JObjectPtr _$invoke( + core$_.int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses(0, descriptor.address, args.address), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + core$_.int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'onResult(Z)V') { + _$impls[$p]!.onResult( + ($a![0] as jni$_.JBoolean).toDartBool(releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $AndroidLocalNetworkPlugin$PermissionCallback $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'com.example.android_local_network.AndroidLocalNetworkPlugin$PermissionCallback', + $p, + _$invokePointer, + [if ($impl.onResult$async) r'onResult(Z)V'], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory AndroidLocalNetworkPlugin$PermissionCallback.implement( + $AndroidLocalNetworkPlugin$PermissionCallback $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return $i.implement(); + } +} + +extension AndroidLocalNetworkPlugin$PermissionCallback$$Methods + on AndroidLocalNetworkPlugin$PermissionCallback { + static final _id_onResult = AndroidLocalNetworkPlugin$PermissionCallback + ._class + .instanceMethodId(r'onResult', r'(Z)V'); + + static final _onResult = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int32,)>, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + ) + >(); + + /// from: `public abstract void onResult(boolean granted)` + void onResult(core$_.bool granted) { + _onResult(reference.pointer, _id_onResult.pointer, granted ? 1 : 0).check(); + } +} + +abstract base mixin class $AndroidLocalNetworkPlugin$PermissionCallback { + factory $AndroidLocalNetworkPlugin$PermissionCallback({ + required void Function(core$_.bool granted) onResult, + core$_.bool onResult$async, + }) = _$AndroidLocalNetworkPlugin$PermissionCallback; + + void onResult(core$_.bool granted); + core$_.bool get onResult$async => false; +} + +final class _$AndroidLocalNetworkPlugin$PermissionCallback + with $AndroidLocalNetworkPlugin$PermissionCallback { + _$AndroidLocalNetworkPlugin$PermissionCallback({ + required void Function(core$_.bool granted) onResult, + this.onResult$async = false, + }) : _onResult = onResult; + + final void Function(core$_.bool granted) _onResult; + final core$_.bool onResult$async; + + void onResult(core$_.bool granted) { + return _onResult(granted); + } +} + +final class $AndroidLocalNetworkPlugin$PermissionCallback$Type$ + extends jni$_.JType { + @jni$_.internal + const $AndroidLocalNetworkPlugin$PermissionCallback$Type$(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/example/android_local_network/AndroidLocalNetworkPlugin$PermissionCallback;'; +} diff --git a/packages/android_local_network/pubspec.yaml b/packages/android_local_network/pubspec.yaml new file mode 100644 index 000000000000..ea0b42a3a22e --- /dev/null +++ b/packages/android_local_network/pubspec.yaml @@ -0,0 +1,27 @@ +name: android_local_network +description: A Flutter package to handle Android 17 Local Area Permission (ACCESS_LOCAL_NETWORK) for Dart sockets. +repository: https://github.com/flutter/packages/tree/main/packages/android_local_network +version: 0.1.1 + +environment: + sdk: ^3.9.0 + flutter: ">=3.35.0" + +flutter: + plugin: + platforms: + android: + package: com.example.android_local_network + pluginClass: AndroidLocalNetworkPlugin + +dependencies: + flutter: + sdk: flutter + jni: ^1.0.0 + jni_flutter: ^1.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + jnigen: ^0.16.0 + test: ^1.16.0 diff --git a/packages/android_local_network/test/android_local_network_test.dart b/packages/android_local_network/test/android_local_network_test.dart new file mode 100644 index 000000000000..79725bb7ffc8 --- /dev/null +++ b/packages/android_local_network/test/android_local_network_test.dart @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors +// 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:android_local_network/android_local_network.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('checkPermission returns true on non-Android', () async { + if (!Platform.isAndroid) { + expect(await AndroidLocalNetwork.checkPermission(), isTrue); + } + }); + + test('requestPermission returns true on non-Android', () async { + if (!Platform.isAndroid) { + expect(await AndroidLocalNetwork.requestPermission(), isTrue); + } + }); +} diff --git a/packages/android_local_network/tool/jnigen.dart b/packages/android_local_network/tool/jnigen.dart new file mode 100644 index 000000000000..f33ca4995f0c --- /dev/null +++ b/packages/android_local_network/tool/jnigen.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors +// 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:jnigen/jnigen.dart'; + +void main() { + final Uri packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/src/android_local_network.g.dart'), + structure: OutputStructure.singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig( + addGradleDeps: true, + androidExample: 'example/', + ), + sourcePath: [ + packageRoot.resolve('android/src/main/java/'), + ], + classes: [ + 'com.example.android_local_network.AndroidLocalNetworkPlugin', + ], + ), + ); +}