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',
+ ],
+ ),
+ );
+}