From 72614676f845e2d6900b31221b19b55a9c20a786 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Tue, 12 Aug 2025 15:28:04 +0200 Subject: [PATCH 01/12] [jni] Fix the context when using multiple flutter engines --- pkgs/jni/CHANGELOG.md | 15 +- .../com/github/dart_lang/jni/JniPlugin.java | 73 +- pkgs/jni/example/lib/main.dart | 68 +- pkgs/jni/lib/src/jni.dart | 43 +- pkgs/jni/lib/src/plugin/generated_plugin.dart | 636 ++++++++++++++++++ pkgs/jni/src/dartjni.c | 75 +-- pkgs/jni/src/dartjni.h | 39 +- pkgs/jni/tool/generate_jni_bindings.dart | 29 + 8 files changed, 852 insertions(+), 126 deletions(-) create mode 100644 pkgs/jni/lib/src/plugin/generated_plugin.dart create mode 100644 pkgs/jni/tool/generate_jni_bindings.dart diff --git a/pkgs/jni/CHANGELOG.md b/pkgs/jni/CHANGELOG.md index 32074f7c65..36db2b5be1 100644 --- a/pkgs/jni/CHANGELOG.md +++ b/pkgs/jni/CHANGELOG.md @@ -3,6 +3,10 @@ - **Breaking Change**: Made `Jni.env` internal. - **Breaking Change**: Renamed `JObjType` to `JType`. - **Breaking Change**: Made all of the type classes internal. +- **Breaking Change**: Removed `Jni.getApplicationClassLoader()`, + `Jni.getCurrentActivity()`, and `Jni.getCachedApplicationContext()`. Instead + use `Jni.applicationContext(engineId)` to access the application context and + listen to `Jni.activityStream(engineId)` to acccess the activity. - Update to the latest lints. ## 0.14.2 @@ -13,12 +17,13 @@ ## 0.14.1 -- Updated `bin/setup.dart` to use Gradle instead of Maven for building Java sources. Added gradle executables - and bootstrap jars [#2003](https://github.com/dart-lang/native/issues/2003) -- Added `JObject.isInstanceOf` which checks whether a `JObject` is an instance +- Updated `bin/setup.dart` to use Gradle instead of Maven for building Java + sources. Added gradle executables and bootstrap jars + [#2003](https://github.com/dart-lang/native/issues/2003) +- Added `JObject.isInstanceOf` which checks whether a `JObject` is an instance of a java class. -- Fixed a [bug](https://github.com/dart-lang/native/issues/1908) where - Java interfaces implemented in on the main thread in Dart could deadlock when +- Fixed a [bug](https://github.com/dart-lang/native/issues/1908) where Java + interfaces implemented in on the main thread in Dart could deadlock when invoked from the main thread outside the context of a Dart isolate. ## 0.14.0 diff --git a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java index ba41a22250..a0cf533f25 100644 --- a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java +++ b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java @@ -10,45 +10,88 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class JniPlugin implements FlutterPlugin, ActivityAware { + private static final ConcurrentHashMap pluginMap = new ConcurrentHashMap<>(); + + private long engineId; + private Context context; + private Activity activity; + private final List activityListeners = new ArrayList<>(); + + public static @NonNull Context getApplicationContext(long engineId) { + return Objects.requireNonNull(pluginMap.get(engineId)).context; + } + + public interface ActivityListener { + void onActivityChanged(Activity activity); + } + + public static void addActivityListener(long engineId, @NonNull ActivityListener listener) { + var plugin = Objects.requireNonNull(pluginMap.get(engineId)); + plugin.activityListeners.add(listener); + listener.onActivityChanged(plugin.activity); + } + + public static void removeActivityListener(long engineId, @NonNull ActivityListener listener) { + var plugin = Objects.requireNonNull(pluginMap.get(engineId)); + plugin.activityListeners.remove(listener); + } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - setup(binding.getApplicationContext()); + //noinspection deprecation + engineId = binding.getFlutterEngine().getEngineId(); + context = binding.getApplicationContext(); + pluginMap.put(engineId, this); } - private void setup(Context context) { - initializeJni(context, getClass().getClassLoader()); + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + context = null; + activity = null; + pluginMap.remove(engineId); } - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} + private void notifyActivityListeners() { + for (var listener : activityListeners) { + listener.onActivityChanged(activity); + } + } + + private void setActivity(Activity newActivity) { + activity = newActivity; + notifyActivityListeners(); + } - // Activity handling methods @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { - Activity activity = binding.getActivity(); - setJniActivity(activity, activity.getApplicationContext()); + setActivity(binding.getActivity()); } @Override - public void onDetachedFromActivityForConfigChanges() {} + public void onDetachedFromActivityForConfigChanges() { + setActivity(null); + } @Override public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { - Activity activity = binding.getActivity(); - setJniActivity(activity, activity.getApplicationContext()); + setActivity(binding.getActivity()); } @Override - public void onDetachedFromActivity() {} - - native void initializeJni(Context context, ClassLoader classLoader); + public void onDetachedFromActivity() { + setActivity(null); + } - native void setJniActivity(Activity activity, Context context); + static native void setClassLoader(ClassLoader classLoader); static { System.loadLibrary("dartjni"); + setClassLoader(JniPlugin.class.getClassLoader()); } } diff --git a/pkgs/jni/example/lib/main.dart b/pkgs/jni/example/lib/main.dart index 29dd29ec93..32e83ee092 100644 --- a/pkgs/jni/example/lib/main.dart +++ b/pkgs/jni/example/lib/main.dart @@ -5,6 +5,7 @@ // ignore_for_file: library_private_types_in_public_api import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:jni/jni.dart'; @@ -31,12 +32,13 @@ String backAndForth() { return dartString; } -void quit() { - JObject.fromReference(Jni.getCurrentActivity()).use((ac) => - ac.jClass.instanceMethodId("finish", "()V").call(ac, jvoid.type, [])); +void quit(JObject activity) { + activity.jClass + .instanceMethodId("finish", "()V") + .call(activity, jvoid.type, []); } -void showToast(String text) { +void showToast(String text, JObject activity) { // This is example for calling your app's custom java code. // Place the Toaster class in the app's android/ source Folder, with a Keep // annotation or appropriate proguard rules to retain classes in release mode. @@ -51,9 +53,9 @@ void showToast(String text) { '(Landroid/app/Activity;Landroid/content/Context;' 'Ljava/lang/CharSequence;I)' 'Lcom/github/dart_lang/jni_example/Toaster;'); - final toaster = makeText.call(toasterClass, JObject.type, [ - Jni.getCurrentActivity(), - Jni.getCachedApplicationContext(), + final toaster = makeText(toasterClass, JObject.type, [ + activity, + Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!), '😀'.toJString(), 0, ]); @@ -62,16 +64,17 @@ void showToast(String text) { } void main() { + WidgetsFlutterBinding.ensureInitialized(); if (!Platform.isAndroid) { Jni.spawn(); } final examples = [ - Example("Math.random()", () => randomDouble(), runInitially: false), + Example("Math.random()", (_) => randomDouble(), runInitially: false), if (Platform.isAndroid) ...[ Example("Minutes of usage since reboot", - () => (uptime() / (60 * 1000)).floor()), - Example("Back and forth string conversion", () => backAndForth()), - Example("Device name", () { + (_) => (uptime() / (60 * 1000)).floor()), + Example("Back and forth string conversion", (_) => backAndForth()), + Example("Device name", (_) { final buildClass = JClass.forName("android/os/Build"); return buildClass .staticFieldId("DEVICE", JString.type.signature) @@ -80,13 +83,15 @@ void main() { }), Example( "Package name", - () => JObject.fromReference(Jni.getCurrentActivity()).use((activity) => - activity.jClass - .instanceMethodId("getPackageName", "()Ljava/lang/String;") - .call(activity, JString.type, [])), + (activity) => activity.jClass + .instanceMethodId("getPackageName", "()Ljava/lang/String;") + .call(activity, JString.type, []), + ), + Example( + "Show toast", + (activity) => showToast("Hello from JNI!", activity), + runInitially: false, ), - Example("Show toast", () => showToast("Hello from JNI!"), - runInitially: false), Example( "Quit", quit, @@ -99,7 +104,7 @@ void main() { class Example { String title; - dynamic Function() callback; + dynamic Function(JObject activity) callback; bool runInitially; Example(this.title, this.callback, {this.runInitially = true}); } @@ -113,8 +118,12 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + late Stream activityStream; + @override void initState() { + activityStream = + Jni.androidActivities(PlatformDispatcher.instance.engineId!); super.initState(); } @@ -125,11 +134,19 @@ class _MyAppState extends State { appBar: AppBar( title: const Text('JNI Examples'), ), - body: ListView.builder( - itemCount: widget.examples.length, - itemBuilder: (context, i) { - final eg = widget.examples[i]; - return ExampleCard(eg); + body: StreamBuilder( + stream: activityStream, + builder: (_, snapshot) { + if (!snapshot.hasData || snapshot.data == null) { + return Container(); + } + return ListView.builder( + itemCount: widget.examples.length, + itemBuilder: (context, i) { + final eg = widget.examples[i]; + return ExampleCard(eg, snapshot.data!); + }, + ); }), ), ); @@ -137,8 +154,9 @@ class _MyAppState extends State { } class ExampleCard extends StatefulWidget { - const ExampleCard(this.example, {super.key}); + const ExampleCard(this.example, this.activity, {super.key}); final Example example; + final JObject activity; @override _ExampleCardState createState() => _ExampleCardState(); @@ -165,7 +183,7 @@ class _ExampleCardState extends State { var hasError = false; if (_run) { try { - result = eg.callback().toString(); + result = eg.callback(widget.activity).toString(); } on Exception catch (e) { hasError = true; result = e.toString(); diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart index 3c7191d910..4ab28008fb 100644 --- a/pkgs/jni/lib/src/jni.dart +++ b/pkgs/jni/lib/src/jni.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; @@ -14,6 +15,7 @@ import 'accessors.dart'; import 'errors.dart'; import 'jobject.dart'; import 'jreference.dart'; +import 'plugin/generated_plugin.dart'; import 'third_party/generated_bindings.dart'; import 'types.dart'; @@ -212,22 +214,37 @@ abstract final class Jni { @internal static final env = GlobalJniEnv(_fetchGlobalEnv()); - /// Returns current application context on Android. - static JReference getCachedApplicationContext() { - return JGlobalReference(_bindings.GetApplicationContext()); + /// Android application context. + /// + /// Pass `PlatformDispatcher.instance.engineId` to the [engineId] field. + static JObject androidApplicationContext(int engineId) { + return JniPlugin.getApplicationContext(engineId); } - /// Returns current activity. - static JReference getCurrentActivity() => - JGlobalReference(_bindings.GetCurrentActivity()); - - /// Get the initial classLoader of the application. + /// A stream of Android activities. /// - /// This is especially useful on Android, where - /// JNI threads cannot access application classes using - /// the usual `JniEnv.FindClass` method. - static JReference getApplicationClassLoader() => - JGlobalReference(_bindings.GetClassLoader()); + /// Pass `PlatformDispatcher.instance.engineId` to the [engineId] field. + static Stream androidActivities(int engineId) { + late final StreamController androidActivitiesController; + final activityListener = JniPlugin$ActivityListener.implement( + $JniPlugin$ActivityListener( + onActivityChanged: (activity) { + androidActivitiesController.add(activity); + }, + ), + ); + androidActivitiesController = StreamController( + sync: true, + onListen: () { + JniPlugin.addActivityListener(engineId, activityListener); + }, + onCancel: () { + JniPlugin.removeActivityListener(engineId, activityListener); + activityListener.release(); + }, + ); + return androidActivitiesController.stream; + } } /// Extensions for use by JNIgen generated code. diff --git a/pkgs/jni/lib/src/plugin/generated_plugin.dart b/pkgs/jni/lib/src/plugin/generated_plugin.dart new file mode 100644 index 0000000000..e7dc87fa58 --- /dev/null +++ b/pkgs/jni/lib/src/plugin/generated_plugin.dart @@ -0,0 +1,636 @@ +// AUTO GENERATED BY JNIGEN 0.15.0. DO NOT EDIT! + +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: prefer_relative_imports +// 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' show Object, String, bool, double, int; +import 'dart:core' as core$_; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + +/// from: `com.github.dart_lang.jni.JniPlugin$ActivityListener` +class JniPlugin$ActivityListener extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + JniPlugin$ActivityListener.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = jni$_.JClass.forName( + r'com/github/dart_lang/jni/JniPlugin$ActivityListener'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $JniPlugin$ActivityListener$NullableType(); + static const type = $JniPlugin$ActivityListener$Type(); + static final _id_onActivityChanged = _class.instanceMethodId( + r'onActivityChanged', + r'(Landroid/app/Activity;)V', + ); + + static final _onActivityChanged = 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 abstract void onActivityChanged(android.app.Activity activity)` + void onActivityChanged( + jni$_.JObject? activity, + ) { + final _$activity = activity?.reference ?? jni$_.jNullReference; + _onActivityChanged(reference.pointer, + _id_onActivityChanged as jni$_.JMethodIDPtr, _$activity.pointer) + .check(); + } + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + 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( + int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'onActivityChanged(Landroid/app/Activity;)V') { + _$impls[$p]!.onActivityChanged( + $a![0]?.as(const jni$_.JObjectType(), releaseOriginal: true), + ); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $JniPlugin$ActivityListener $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.github.dart_lang.jni.JniPlugin$ActivityListener', + $p, + _$invokePointer, + [ + if ($impl.onActivityChanged$async) + r'onActivityChanged(Landroid/app/Activity;)V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory JniPlugin$ActivityListener.implement( + $JniPlugin$ActivityListener $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return JniPlugin$ActivityListener.fromReference( + $i.implementReference(), + ); + } +} + +abstract base mixin class $JniPlugin$ActivityListener { + factory $JniPlugin$ActivityListener({ + required void Function(jni$_.JObject? activity) onActivityChanged, + bool onActivityChanged$async, + }) = _$JniPlugin$ActivityListener; + + void onActivityChanged(jni$_.JObject? activity); + bool get onActivityChanged$async => false; +} + +final class _$JniPlugin$ActivityListener with $JniPlugin$ActivityListener { + _$JniPlugin$ActivityListener({ + required void Function(jni$_.JObject? activity) onActivityChanged, + this.onActivityChanged$async = false, + }) : _onActivityChanged = onActivityChanged; + + final void Function(jni$_.JObject? activity) _onActivityChanged; + final bool onActivityChanged$async; + + void onActivityChanged(jni$_.JObject? activity) { + return _onActivityChanged(activity); + } +} + +final class $JniPlugin$ActivityListener$NullableType + extends jni$_.JObjType { + @jni$_.internal + const $JniPlugin$ActivityListener$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jni/JniPlugin$ActivityListener;'; + + @jni$_.internal + @core$_.override + JniPlugin$ActivityListener? fromReference(jni$_.JReference reference) => + reference.isNull + ? null + : JniPlugin$ActivityListener.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($JniPlugin$ActivityListener$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($JniPlugin$ActivityListener$NullableType) && + other is $JniPlugin$ActivityListener$NullableType; + } +} + +final class $JniPlugin$ActivityListener$Type + extends jni$_.JObjType { + @jni$_.internal + const $JniPlugin$ActivityListener$Type(); + + @jni$_.internal + @core$_.override + String get signature => + r'Lcom/github/dart_lang/jni/JniPlugin$ActivityListener;'; + + @jni$_.internal + @core$_.override + JniPlugin$ActivityListener fromReference(jni$_.JReference reference) => + JniPlugin$ActivityListener.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $JniPlugin$ActivityListener$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($JniPlugin$ActivityListener$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($JniPlugin$ActivityListener$Type) && + other is $JniPlugin$ActivityListener$Type; + } +} + +/// from: `com.github.dart_lang.jni.JniPlugin` +class JniPlugin extends jni$_.JObject { + @jni$_.internal + @core$_.override + final jni$_.JObjType $type; + + @jni$_.internal + JniPlugin.fromReference( + jni$_.JReference reference, + ) : $type = type, + super.fromReference(reference); + + static final _class = + jni$_.JClass.forName(r'com/github/dart_lang/jni/JniPlugin'); + + /// The type which includes information such as the signature of this class. + static const nullableType = $JniPlugin$NullableType(); + static const type = $JniPlugin$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 JniPlugin() { + return JniPlugin.fromReference( + _new$(_class.reference.pointer, _id_new$ as jni$_.JMethodIDPtr) + .reference); + } + + static final _id_getApplicationContext = _class.staticMethodId( + r'getApplicationContext', + r'(J)Landroid/content/Context;', + ); + + static final _getApplicationContext = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int64,)>)>>( + 'globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); + + /// from: `static public android.content.Context getApplicationContext(long j)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject getApplicationContext( + int j, + ) { + return _getApplicationContext(_class.reference.pointer, + _id_getApplicationContext as jni$_.JMethodIDPtr, j) + .object(const jni$_.JObjectType()); + } + + static final _id_addActivityListener = _class.staticMethodId( + r'addActivityListener', + r'(JLcom/github/dart_lang/jni/JniPlugin$ActivityListener;)V', + ); + + static final _addActivityListener = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_ + .VarArgs<(jni$_.Int64, jni$_.Pointer)>)>>( + 'globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + + /// from: `static public void addActivityListener(long j, com.github.dart_lang.jni.JniPlugin$ActivityListener activityListener)` + static void addActivityListener( + int j, + JniPlugin$ActivityListener activityListener, + ) { + final _$activityListener = activityListener.reference; + _addActivityListener( + _class.reference.pointer, + _id_addActivityListener as jni$_.JMethodIDPtr, + j, + _$activityListener.pointer) + .check(); + } + + static final _id_removeActivityListener = _class.staticMethodId( + r'removeActivityListener', + r'(JLcom/github/dart_lang/jni/JniPlugin$ActivityListener;)V', + ); + + static final _removeActivityListener = jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_ + .VarArgs<(jni$_.Int64, jni$_.Pointer)>)>>( + 'globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function(jni$_.Pointer, + jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + + /// from: `static public void removeActivityListener(long j, com.github.dart_lang.jni.JniPlugin$ActivityListener activityListener)` + static void removeActivityListener( + int j, + JniPlugin$ActivityListener activityListener, + ) { + final _$activityListener = activityListener.reference; + _removeActivityListener( + _class.reference.pointer, + _id_removeActivityListener as jni$_.JMethodIDPtr, + j, + _$activityListener.pointer) + .check(); + } + + static final _id_onAttachedToEngine = _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 flutterPluginBinding)` + void onAttachedToEngine( + jni$_.JObject flutterPluginBinding, + ) { + final _$flutterPluginBinding = flutterPluginBinding.reference; + _onAttachedToEngine( + reference.pointer, + _id_onAttachedToEngine as jni$_.JMethodIDPtr, + _$flutterPluginBinding.pointer) + .check(); + } + + static final _id_onDetachedFromEngine = _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 flutterPluginBinding)` + void onDetachedFromEngine( + jni$_.JObject flutterPluginBinding, + ) { + final _$flutterPluginBinding = flutterPluginBinding.reference; + _onDetachedFromEngine( + reference.pointer, + _id_onDetachedFromEngine as jni$_.JMethodIDPtr, + _$flutterPluginBinding.pointer) + .check(); + } + + static final _id_onAttachedToActivity = _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 activityPluginBinding)` + void onAttachedToActivity( + jni$_.JObject activityPluginBinding, + ) { + final _$activityPluginBinding = activityPluginBinding.reference; + _onAttachedToActivity( + reference.pointer, + _id_onAttachedToActivity as jni$_.JMethodIDPtr, + _$activityPluginBinding.pointer) + .check(); + } + + static final _id_onDetachedFromActivityForConfigChanges = + _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 as jni$_.JMethodIDPtr) + .check(); + } + + static final _id_onReattachedToActivityForConfigChanges = + _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 activityPluginBinding)` + void onReattachedToActivityForConfigChanges( + jni$_.JObject activityPluginBinding, + ) { + final _$activityPluginBinding = activityPluginBinding.reference; + _onReattachedToActivityForConfigChanges( + reference.pointer, + _id_onReattachedToActivityForConfigChanges as jni$_.JMethodIDPtr, + _$activityPluginBinding.pointer) + .check(); + } + + static final _id_onDetachedFromActivity = _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 as jni$_.JMethodIDPtr) + .check(); + } +} + +final class $JniPlugin$NullableType extends jni$_.JObjType { + @jni$_.internal + const $JniPlugin$NullableType(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jni/JniPlugin;'; + + @jni$_.internal + @core$_.override + JniPlugin? fromReference(jni$_.JReference reference) => reference.isNull + ? null + : JniPlugin.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => this; + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($JniPlugin$NullableType).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($JniPlugin$NullableType) && + other is $JniPlugin$NullableType; + } +} + +final class $JniPlugin$Type extends jni$_.JObjType { + @jni$_.internal + const $JniPlugin$Type(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/github/dart_lang/jni/JniPlugin;'; + + @jni$_.internal + @core$_.override + JniPlugin fromReference(jni$_.JReference reference) => + JniPlugin.fromReference( + reference, + ); + @jni$_.internal + @core$_.override + jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + + @jni$_.internal + @core$_.override + jni$_.JObjType get nullableType => + const $JniPlugin$NullableType(); + + @jni$_.internal + @core$_.override + final superCount = 1; + + @core$_.override + int get hashCode => ($JniPlugin$Type).hashCode; + + @core$_.override + bool operator ==(Object other) { + return other.runtimeType == ($JniPlugin$Type) && other is $JniPlugin$Type; + } +} diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index fe0651fe12..79fa978227 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -8,8 +8,9 @@ #include "dartjni.h" -#ifndef _WIN32 +#if !defined(_WIN32) pthread_key_t tlsKey; +pthread_mutex_t spawnLock = PTHREAD_MUTEX_INITIALIZER; #endif jclass FindClassUnchecked(const char* name) { @@ -47,8 +48,6 @@ JniContext jni_context = { .jvm = NULL, .classLoader = NULL, .loadClassMethod = NULL, - .appContext = NULL, - .currentActivity = NULL, }; JniContext* jni = &jni_context; @@ -56,7 +55,7 @@ THREAD_LOCAL JNIEnv* jniEnv = NULL; JniExceptionMethods exceptionMethods; void init() { -#ifndef _WIN32 +#if !defined(_WIN32) // Init TLS keys. pthread_key_create(&tlsKey, detach_thread); #endif @@ -83,7 +82,7 @@ void init() { } void deinit() { -#ifndef _WIN32 +#if !defined(_WIN32) // Delete TLS keys. pthread_key_delete(tlsKey); #endif @@ -120,30 +119,16 @@ jobject GetClassLoader() { return (*jniEnv)->NewGlobalRef(jniEnv, jni_context.classLoader); } -FFI_PLUGIN_EXPORT -jobject GetApplicationContext() { - attach_thread(); - return (*jniEnv)->NewGlobalRef(jniEnv, jni_context.appContext); -} - -FFI_PLUGIN_EXPORT -jobject GetCurrentActivity() { - attach_thread(); - return (*jniEnv)->NewGlobalRef(jniEnv, jni_context.currentActivity); -} - // JNI Initialization -#ifdef __ANDROID__ +#if defined(__ANDROID__) JNIEXPORT void JNICALL -Java_com_github_dart_1lang_jni_JniPlugin_initializeJni(JNIEnv* env, - jobject obj, - jobject appContext, - jobject classLoader) { +Java_com_github_dart_1lang_jni_JniPlugin_setClassLoader(JNIEnv* env, + jclass clazz, + jobject classLoader) { jniEnv = env; (*env)->GetJavaVM(env, &jni_context.jvm); jni_context.classLoader = (*env)->NewGlobalRef(env, classLoader); - jni_context.appContext = (*env)->NewGlobalRef(env, appContext); jclass classLoaderClass = (*env)->GetObjectClass(env, classLoader); jni_context.loadClassMethod = (*env)->GetMethodID(env, classLoaderClass, "loadClass", @@ -151,22 +136,6 @@ Java_com_github_dart_1lang_jni_JniPlugin_initializeJni(JNIEnv* env, init(); } -JNIEXPORT void JNICALL -Java_com_github_dart_1lang_jni_JniPlugin_setJniActivity(JNIEnv* env, - jobject obj, - jobject activity, - jobject context) { - jniEnv = env; - if (jni_context.currentActivity != NULL) { - (*env)->DeleteGlobalRef(env, jni_context.currentActivity); - } - jni_context.currentActivity = (*env)->NewGlobalRef(env, activity); - if (jni_context.appContext != NULL) { - (*env)->DeleteGlobalRef(env, jni_context.appContext); - } - jni_context.appContext = (*env)->NewGlobalRef(env, context); -} - // Sometimes you may get linker error trying to link JNI_CreateJavaVM APIs // on Android NDK. So IFDEF is required. #else @@ -220,6 +189,34 @@ JniErrorCode SpawnJvm(JavaVMInitArgs* initArgs) { return JNI_OK; } #endif +#if defined(_WIN32) +// Pre-initialization of critical section on windows - this is required because +// there's no coordination between multiple isolates calling Spawn. +// +// Taken from https://stackoverflow.com/a/12858955 +CRITICAL_SECTION spawnLock = {0}; +BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module + DWORD fdwReason, // reason for calling function + LPVOID lpReserved) { // reserved + switch (fdwReason) { + case DLL_PROCESS_ATTACH: + // Initialize once for each new process. + // Return FALSE to fail DLL load. + InitializeCriticalSection(&spawnLock); + break; + case DLL_THREAD_DETACH: + if (jniEnv != NULL) { + detach_thread(jniEnv); + } + break; + case DLL_PROCESS_DETACH: + // Perform any necessary cleanup. + DeleteCriticalSection(&spawnLock); + break; + } + return TRUE; // Successful DLL_PROCESS_ATTACH. +} +#endif FFI_PLUGIN_EXPORT JniExceptionDetails GetExceptionDetails(jthrowable exception) { diff --git a/pkgs/jni/src/dartjni.h b/pkgs/jni/src/dartjni.h index fcda339a35..6d14ae70cc 100644 --- a/pkgs/jni/src/dartjni.h +++ b/pkgs/jni/src/dartjni.h @@ -13,30 +13,8 @@ #include #include -#ifdef _WIN32 -#include -#else -#include -#include -#endif - -#ifdef _WIN32 -#define FFI_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FFI_PLUGIN_EXPORT -#endif - -#ifdef _WIN32 -#define THREAD_LOCAL __declspec(thread) -#else -#define THREAD_LOCAL __thread -#endif - -#ifdef __ANDROID__ +#if defined(__ANDROID__) #include -#endif - -#ifdef __ANDROID__ #define __ENVP_CAST (JNIEnv**) #else #define __ENVP_CAST (void**) @@ -44,8 +22,10 @@ /// Locking functions for windows and pthread. -#ifdef _WIN32 +#if defined(_WIN32) #include +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#define THREAD_LOCAL __declspec(thread) typedef SRWLOCK MutexLock; typedef CONDITION_VARIABLE ConditionVariable; @@ -88,6 +68,9 @@ static inline void free_mem(void* mem) { #else #include +#include +#define FFI_PLUGIN_EXPORT +#define THREAD_LOCAL __thread typedef pthread_mutex_t MutexLock; typedef pthread_cond_t ConditionVariable; @@ -145,8 +128,6 @@ typedef struct JniContext { JavaVM* jvm; jobject classLoader; jmethodID loadClassMethod; - jobject currentActivity; - jobject appContext; JniLocks locks; } JniContext; @@ -157,7 +138,7 @@ extern THREAD_LOCAL JNIEnv* jniEnv; extern JniContext* jni; /// Handling the lifetime of thread-local jniEnv. -#ifndef _WIN32 +#if !defined(_WIN32) extern pthread_key_t tlsKey; #endif @@ -172,7 +153,7 @@ static inline void detach_thread(void* data) { static inline void attach_thread() { if (jniEnv == NULL) { (*jni->jvm)->AttachCurrentThread(jni->jvm, __ENVP_CAST & jniEnv, NULL); -#ifndef _WIN32 +#if !defined(_WIN32) pthread_setspecific(tlsKey, &jniEnv); #endif } @@ -253,7 +234,7 @@ FFI_PLUGIN_EXPORT jobject GetCurrentActivity(void); /// Load class into [cls] using platform specific mechanism static inline void load_class_platform(jclass* cls, const char* name) { -#ifdef __ANDROID__ +#if defined(__ANDROID__) jstring className = (*jniEnv)->NewStringUTF(jniEnv, name); *cls = (*jniEnv)->CallObjectMethod(jniEnv, jni->classLoader, jni->loadClassMethod, className); diff --git a/pkgs/jni/tool/generate_jni_bindings.dart b/pkgs/jni/tool/generate_jni_bindings.dart new file mode 100644 index 0000000000..126232c7c2 --- /dev/null +++ b/pkgs/jni/tool/generate_jni_bindings.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:jnigen/jnigen.dart'; + +void main() { + generateJniBindings( + Config( + androidSdkConfig: AndroidSdkConfig( + addGradleDeps: true, + androidExample: 'example/', + ), + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: Uri.file('lib/src/plugin/generated_plugin.dart'), + structure: OutputStructure.singleFile, + ), + ), + classes: ['com.github.dart_lang.jni.JniPlugin'], + preamble: ''' +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: prefer_relative_imports''', + ), + ); +} From 44c4805cebd4f2cb41594125c560701c3f154fd8 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Tue, 12 Aug 2025 16:21:17 +0200 Subject: [PATCH 02/12] Fix jnigen examples --- pkgs/jni/CHANGELOG.md | 5 +- pkgs/jni/src/dartjni.c | 58 +++++------ pkgs/jnigen/example/in_app_java/lib/main.dart | 59 ++++++++---- .../android/app/src/debug/AndroidManifest.xml | 1 + .../android/app/src/main/AndroidManifest.xml | 3 +- .../app/src/profile/AndroidManifest.xml | 1 + .../notification_plugin/example/lib/main.dart | 96 ++++++++++++------- pkgs/jnigen/pubspec.yaml | 2 +- 8 files changed, 141 insertions(+), 84 deletions(-) diff --git a/pkgs/jni/CHANGELOG.md b/pkgs/jni/CHANGELOG.md index 36db2b5be1..9e15590f39 100644 --- a/pkgs/jni/CHANGELOG.md +++ b/pkgs/jni/CHANGELOG.md @@ -5,8 +5,9 @@ - **Breaking Change**: Made all of the type classes internal. - **Breaking Change**: Removed `Jni.getApplicationClassLoader()`, `Jni.getCurrentActivity()`, and `Jni.getCachedApplicationContext()`. Instead - use `Jni.applicationContext(engineId)` to access the application context and - listen to `Jni.activityStream(engineId)` to acccess the activity. + use `Jni.androidApplicationContext(engineId)` to access the application + context and listen to `Jni.androidActivities(engineId)` to acccess the + activity. - Update to the latest lints. ## 0.14.2 diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index 79fa978227..67893f9dcf 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -119,8 +119,36 @@ jobject GetClassLoader() { return (*jniEnv)->NewGlobalRef(jniEnv, jni_context.classLoader); } -// JNI Initialization +#if defined(_WIN32) +// Pre-initialization of critical section on windows - this is required because +// there's no coordination between multiple isolates calling Spawn. +// +// Taken from https://stackoverflow.com/a/12858955 +CRITICAL_SECTION spawnLock = {0}; +BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module + DWORD fdwReason, // reason for calling function + LPVOID lpReserved) { // reserved + switch (fdwReason) { + case DLL_PROCESS_ATTACH: + // Initialize once for each new process. + // Return FALSE to fail DLL load. + InitializeCriticalSection(&spawnLock); + break; + case DLL_THREAD_DETACH: + if (jniEnv != NULL) { + detach_thread(jniEnv); + } + break; + case DLL_PROCESS_DETACH: + // Perform any necessary cleanup. + DeleteCriticalSection(&spawnLock); + break; + } + return TRUE; // Successful DLL_PROCESS_ATTACH. +} +#endif +// JNI Initialization #if defined(__ANDROID__) JNIEXPORT void JNICALL Java_com_github_dart_1lang_jni_JniPlugin_setClassLoader(JNIEnv* env, @@ -189,34 +217,6 @@ JniErrorCode SpawnJvm(JavaVMInitArgs* initArgs) { return JNI_OK; } #endif -#if defined(_WIN32) -// Pre-initialization of critical section on windows - this is required because -// there's no coordination between multiple isolates calling Spawn. -// -// Taken from https://stackoverflow.com/a/12858955 -CRITICAL_SECTION spawnLock = {0}; -BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module - DWORD fdwReason, // reason for calling function - LPVOID lpReserved) { // reserved - switch (fdwReason) { - case DLL_PROCESS_ATTACH: - // Initialize once for each new process. - // Return FALSE to fail DLL load. - InitializeCriticalSection(&spawnLock); - break; - case DLL_THREAD_DETACH: - if (jniEnv != NULL) { - detach_thread(jniEnv); - } - break; - case DLL_PROCESS_DETACH: - // Perform any necessary cleanup. - DeleteCriticalSection(&spawnLock); - break; - } - return TRUE; // Successful DLL_PROCESS_ATTACH. -} -#endif FFI_PLUGIN_EXPORT JniExceptionDetails GetExceptionDetails(jthrowable exception) { diff --git a/pkgs/jnigen/example/in_app_java/lib/main.dart b/pkgs/jnigen/example/in_app_java/lib/main.dart index adab59c7d4..adb28abffa 100644 --- a/pkgs/jnigen/example/in_app_java/lib/main.dart +++ b/pkgs/jnigen/example/in_app_java/lib/main.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:jni/jni.dart'; @@ -9,8 +11,9 @@ import 'package:jni/jni.dart'; // structure. import 'android_utils.g.dart'; -JObject activity = JObject.fromReference(Jni.getCurrentActivity()); -JObject context = JObject.fromReference(Jni.getCachedApplicationContext()); +JObject context = Jni.androidApplicationContext( + PlatformDispatcher.instance.engineId!, +); final hashmap = HashMap(K: JString.type, V: JString.type); @@ -26,7 +29,7 @@ const sunglassEmoji = "😎"; /// Display device model number and the number of times this was called /// as Toast. -void showToast() { +void showToast(JObject activity) { final toastCount = hashmap.getOrDefault( "toastCount".toJString(), 0.toJString(), @@ -59,26 +62,50 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatelessWidget { +class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + late final Stream activityStream; + + @override + void initState() { + activityStream = Jni.androidActivities( + PlatformDispatcher.instance.engineId!, + ); + super.initState(); + } + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(title)), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - child: const Text('Show Device Model'), - onPressed: () => showToast(), + return StreamBuilder( + stream: activityStream, + builder: (context, asyncSnapshot) { + if (!asyncSnapshot.hasData || asyncSnapshot.data == null) { + return Container(); + } + final activity = asyncSnapshot.data!; + return Scaffold( + appBar: AppBar(title: Text(widget.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: const Text('Show Device Model'), + onPressed: () => showToast(activity), + ), + ], ), - ], - ), - ), + ), + ); + }, ); } } diff --git a/pkgs/jnigen/example/notification_plugin/example/android/app/src/debug/AndroidManifest.xml b/pkgs/jnigen/example/notification_plugin/example/android/app/src/debug/AndroidManifest.xml index 822a20815a..aff0881254 100644 --- a/pkgs/jnigen/example/notification_plugin/example/android/app/src/debug/AndroidManifest.xml +++ b/pkgs/jnigen/example/notification_plugin/example/android/app/src/debug/AndroidManifest.xml @@ -5,4 +5,5 @@ to allow setting breakpoints, to provide hot reload, etc. --> + diff --git a/pkgs/jnigen/example/notification_plugin/example/android/app/src/main/AndroidManifest.xml b/pkgs/jnigen/example/notification_plugin/example/android/app/src/main/AndroidManifest.xml index 60e7eab802..8a9fd685bf 100644 --- a/pkgs/jnigen/example/notification_plugin/example/android/app/src/main/AndroidManifest.xml +++ b/pkgs/jnigen/example/notification_plugin/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ - + diff --git a/pkgs/jnigen/example/notification_plugin/example/android/app/src/profile/AndroidManifest.xml b/pkgs/jnigen/example/notification_plugin/example/android/app/src/profile/AndroidManifest.xml index 822a20815a..aff0881254 100644 --- a/pkgs/jnigen/example/notification_plugin/example/android/app/src/profile/AndroidManifest.xml +++ b/pkgs/jnigen/example/notification_plugin/example/android/app/src/profile/AndroidManifest.xml @@ -5,4 +5,5 @@ to allow setting breakpoints, to provide hot reload, etc. --> + diff --git a/pkgs/jnigen/example/notification_plugin/example/lib/main.dart b/pkgs/jnigen/example/notification_plugin/example/lib/main.dart index 7b978c7ace..11c836ff63 100644 --- a/pkgs/jnigen/example/notification_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/notification_plugin/example/lib/main.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jni/jni.dart'; // The hierarchy created in generated code will mirror the java package @@ -9,11 +10,9 @@ import 'package:jni/jni.dart'; // more customization in future. import 'package:notification_plugin/notifications.dart'; -JObject activity = JObject.fromReference(Jni.getCurrentActivity()); - int i = 0; -void showNotification(String title, String text) { +void showNotification(String title, String text, JObject activity) { i = i + 1; var jTitle = JString.fromString(title); var jText = JString.fromString(text); @@ -41,48 +40,75 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatelessWidget { - MyHomePage({super.key, required this.title}); +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); final String title; + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { final _title = TextEditingController(text: 'Hello from JNI'); final _text = TextEditingController(text: '😀'); + final activityStream = + Jni.androidActivities(PlatformDispatcher.instance.engineId!); + + @override + void dispose() { + _title.dispose(); + _text.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(title), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: _title, - textCapitalization: TextCapitalization.sentences, - decoration: - const InputDecoration(labelText: 'Notification title'), - ), - TextFormField( - controller: _text, - keyboardType: TextInputType.multiline, - minLines: 1, - maxLines: 4, - decoration: - const InputDecoration(labelText: 'Notification text'), - ), - ElevatedButton( - child: const Text('Show Notification'), - onPressed: () => showNotification(_title.text, _text.text), + return StreamBuilder( + stream: activityStream, + builder: (context, asyncSnapshot) { + if (!asyncSnapshot.hasData || asyncSnapshot.data == null) { + return Container(); + } + final activity = asyncSnapshot.data!; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: _title, + textCapitalization: TextCapitalization.sentences, + decoration: + const InputDecoration(labelText: 'Notification title'), + ), + TextFormField( + controller: _text, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 4, + decoration: + const InputDecoration(labelText: 'Notification text'), + ), + ElevatedButton( + child: const Text('Show Notification'), + onPressed: () => showNotification( + _title.text, + _text.text, + activity, + ), + ), + ], ), - ], + ), ), - ), - ), + ); + }, ); } } diff --git a/pkgs/jnigen/pubspec.yaml b/pkgs/jnigen/pubspec.yaml index cd2dd31b73..352f43a922 100644 --- a/pkgs/jnigen/pubspec.yaml +++ b/pkgs/jnigen/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: dev_dependencies: build_runner: ^2.4.12 dart_flutter_team_lints: ^3.5.2 - jni: ^0.13.0 + jni: ^0.14.0 json_serializable: ^6.8.0 test: ^1.25.8 From cbf49afdb829650b152d66f45905066325382143 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Tue, 12 Aug 2025 16:35:09 +0200 Subject: [PATCH 03/12] Fix version of clang-format --- .github/workflows/jnigen.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index ac3b971919..7a8b25e9dc 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -95,7 +95,7 @@ jobs: - name: install clang tools run: | sudo apt-get update -y - sudo apt-get install -y clang-format + sudo apt-get install -y clang-format-20 - name: Install dependencies run: dart pub get - name: build in_app_java APK From f177f2dbaab926b11bb6b4afba90e641e9e5db4e Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Tue, 12 Aug 2025 16:45:56 +0200 Subject: [PATCH 04/12] Fix version of clang-format --- .github/workflows/jnigen.yaml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 7a8b25e9dc..1c7a4250ec 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -90,12 +90,6 @@ jobs: distribution: 'zulu' java-version: '17' cache: maven - ## Committed bindings are formatted with clang-format. - ## So this is required to format generated bindings identically - - name: install clang tools - run: | - sudo apt-get update -y - sudo apt-get install -y clang-format-20 - name: Install dependencies run: dart pub get - name: build in_app_java APK @@ -172,8 +166,11 @@ jobs: working-directory: ./pkgs/jni - name: install clang tools & CMake run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 20 sudo apt-get update -y - sudo apt-get install -y clang-format build-essential cmake + sudo apt-get install -y clang-format-20 build-essential cmake - run: flutter pub get - name: Check formatting run: dart format --output=none --set-exit-if-changed . @@ -419,7 +416,7 @@ jobs: java-version: '17' - run: | sudo apt-get update -y - sudo apt-get install -y ninja-build libgtk-3-dev clang-format + sudo apt-get install -y ninja-build libgtk-3-dev - run: flutter config --enable-linux-desktop - run: dart pub get - name: Generate full bindings From ea24e4d033d9fa8fed10e1901c38075849e5e2c2 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 17 Oct 2025 10:37:22 +0200 Subject: [PATCH 05/12] Rebase --- .github/workflows/jnigen.yaml | 4 + pkgs/jni/example/android/settings.gradle | 2 +- pkgs/jni/lib/src/jni.dart | 2 +- pkgs/jni/lib/src/plugin/generated_plugin.dart | 86 ++++++++++--------- pkgs/jni/pubspec.yaml | 4 +- pkgs/jni/tool/generate_jni_bindings.dart | 5 +- 6 files changed, 59 insertions(+), 44 deletions(-) diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 1c7a4250ec..6186997794 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -221,6 +221,10 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true path-to-lcov: ./pkgs/jni/coverage/lcov.info + - name: regenerate & compare jnigen bindings + run: | + dart run tool/generate_jni_bindings.dart + git diff --exit-code -- lib/src/plugin # TODO(https://github.com/dart-lang/ffigen/issues/555): FFIgen generated # on my machine has macOS specific stuff and CI does not. # We should just generate the struct as opaque, but we currently can't. diff --git a/pkgs/jni/example/android/settings.gradle b/pkgs/jni/example/android/settings.gradle index 27029b3eb8..4f520718dc 100644 --- a/pkgs/jni/example/android/settings.gradle +++ b/pkgs/jni/example/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.6.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.10" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart index 4ab28008fb..a6ef801a32 100644 --- a/pkgs/jni/lib/src/jni.dart +++ b/pkgs/jni/lib/src/jni.dart @@ -228,13 +228,13 @@ abstract final class Jni { late final StreamController androidActivitiesController; final activityListener = JniPlugin$ActivityListener.implement( $JniPlugin$ActivityListener( + onActivityChanged$async: true, onActivityChanged: (activity) { androidActivitiesController.add(activity); }, ), ); androidActivitiesController = StreamController( - sync: true, onListen: () { JniPlugin.addActivityListener(engineId, activityListener); }, diff --git a/pkgs/jni/lib/src/plugin/generated_plugin.dart b/pkgs/jni/lib/src/plugin/generated_plugin.dart index e7dc87fa58..871b77f285 100644 --- a/pkgs/jni/lib/src/plugin/generated_plugin.dart +++ b/pkgs/jni/lib/src/plugin/generated_plugin.dart @@ -35,8 +35,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; @@ -45,7 +45,7 @@ import 'package:jni/jni.dart' as jni$_; class JniPlugin$ActivityListener extends jni$_.JObject { @jni$_.internal @core$_.override - final jni$_.JObjType $type; + final jni$_.JType $type; @jni$_.internal JniPlugin$ActivityListener.fromReference( @@ -57,8 +57,12 @@ class JniPlugin$ActivityListener extends jni$_.JObject { r'com/github/dart_lang/jni/JniPlugin$ActivityListener'); /// The type which includes information such as the signature of this class. - static const nullableType = $JniPlugin$ActivityListener$NullableType(); - static const type = $JniPlugin$ActivityListener$Type(); + static const jni$_.JType nullableType = + $JniPlugin$ActivityListener$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $JniPlugin$ActivityListener$Type$(); static final _id_onActivityChanged = _class.instanceMethodId( r'onActivityChanged', r'(Landroid/app/Activity;)V', @@ -117,7 +121,7 @@ class JniPlugin$ActivityListener extends jni$_.JObject { final $a = $i.args; if ($d == r'onActivityChanged(Landroid/app/Activity;)V') { _$impls[$p]!.onActivityChanged( - $a![0]?.as(const jni$_.JObjectType(), releaseOriginal: true), + $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), ); return jni$_.nullptr; } @@ -190,10 +194,10 @@ final class _$JniPlugin$ActivityListener with $JniPlugin$ActivityListener { } } -final class $JniPlugin$ActivityListener$NullableType - extends jni$_.JObjType { +final class $JniPlugin$ActivityListener$NullableType$ + extends jni$_.JType { @jni$_.internal - const $JniPlugin$ActivityListener$NullableType(); + const $JniPlugin$ActivityListener$NullableType$(); @jni$_.internal @core$_.override @@ -210,30 +214,30 @@ final class $JniPlugin$ActivityListener$NullableType ); @jni$_.internal @core$_.override - jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); @jni$_.internal @core$_.override - jni$_.JObjType get nullableType => this; + jni$_.JType get nullableType => this; @jni$_.internal @core$_.override final superCount = 1; @core$_.override - int get hashCode => ($JniPlugin$ActivityListener$NullableType).hashCode; + int get hashCode => ($JniPlugin$ActivityListener$NullableType$).hashCode; @core$_.override bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$ActivityListener$NullableType) && - other is $JniPlugin$ActivityListener$NullableType; + return other.runtimeType == ($JniPlugin$ActivityListener$NullableType$) && + other is $JniPlugin$ActivityListener$NullableType$; } } -final class $JniPlugin$ActivityListener$Type - extends jni$_.JObjType { +final class $JniPlugin$ActivityListener$Type$ + extends jni$_.JType { @jni$_.internal - const $JniPlugin$ActivityListener$Type(); + const $JniPlugin$ActivityListener$Type$(); @jni$_.internal @core$_.override @@ -248,24 +252,24 @@ final class $JniPlugin$ActivityListener$Type ); @jni$_.internal @core$_.override - jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); @jni$_.internal @core$_.override - jni$_.JObjType get nullableType => - const $JniPlugin$ActivityListener$NullableType(); + jni$_.JType get nullableType => + const $JniPlugin$ActivityListener$NullableType$(); @jni$_.internal @core$_.override final superCount = 1; @core$_.override - int get hashCode => ($JniPlugin$ActivityListener$Type).hashCode; + int get hashCode => ($JniPlugin$ActivityListener$Type$).hashCode; @core$_.override bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$ActivityListener$Type) && - other is $JniPlugin$ActivityListener$Type; + return other.runtimeType == ($JniPlugin$ActivityListener$Type$) && + other is $JniPlugin$ActivityListener$Type$; } } @@ -273,7 +277,7 @@ final class $JniPlugin$ActivityListener$Type class JniPlugin extends jni$_.JObject { @jni$_.internal @core$_.override - final jni$_.JObjType $type; + final jni$_.JType $type; @jni$_.internal JniPlugin.fromReference( @@ -285,8 +289,11 @@ class JniPlugin extends jni$_.JObject { jni$_.JClass.forName(r'com/github/dart_lang/jni/JniPlugin'); /// The type which includes information such as the signature of this class. - static const nullableType = $JniPlugin$NullableType(); - static const type = $JniPlugin$Type(); + static const jni$_.JType nullableType = + $JniPlugin$NullableType$(); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $JniPlugin$Type$(); static final _id_new$ = _class.constructorId( r'()V', ); @@ -332,7 +339,7 @@ class JniPlugin extends jni$_.JObject { ) { return _getApplicationContext(_class.reference.pointer, _id_getApplicationContext as jni$_.JMethodIDPtr, j) - .object(const jni$_.JObjectType()); + .object(const jni$_.$JObject$Type$()); } static final _id_addActivityListener = _class.staticMethodId( @@ -562,9 +569,9 @@ class JniPlugin extends jni$_.JObject { } } -final class $JniPlugin$NullableType extends jni$_.JObjType { +final class $JniPlugin$NullableType$ extends jni$_.JType { @jni$_.internal - const $JniPlugin$NullableType(); + const $JniPlugin$NullableType$(); @jni$_.internal @core$_.override @@ -579,29 +586,29 @@ final class $JniPlugin$NullableType extends jni$_.JObjType { ); @jni$_.internal @core$_.override - jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); @jni$_.internal @core$_.override - jni$_.JObjType get nullableType => this; + jni$_.JType get nullableType => this; @jni$_.internal @core$_.override final superCount = 1; @core$_.override - int get hashCode => ($JniPlugin$NullableType).hashCode; + int get hashCode => ($JniPlugin$NullableType$).hashCode; @core$_.override bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$NullableType) && - other is $JniPlugin$NullableType; + return other.runtimeType == ($JniPlugin$NullableType$) && + other is $JniPlugin$NullableType$; } } -final class $JniPlugin$Type extends jni$_.JObjType { +final class $JniPlugin$Type$ extends jni$_.JType { @jni$_.internal - const $JniPlugin$Type(); + const $JniPlugin$Type$(); @jni$_.internal @core$_.override @@ -615,22 +622,21 @@ final class $JniPlugin$Type extends jni$_.JObjType { ); @jni$_.internal @core$_.override - jni$_.JObjType get superType => const jni$_.JObjectNullableType(); + jni$_.JType get superType => const jni$_.$JObject$NullableType$(); @jni$_.internal @core$_.override - jni$_.JObjType get nullableType => - const $JniPlugin$NullableType(); + jni$_.JType get nullableType => const $JniPlugin$NullableType$(); @jni$_.internal @core$_.override final superCount = 1; @core$_.override - int get hashCode => ($JniPlugin$Type).hashCode; + int get hashCode => ($JniPlugin$Type$).hashCode; @core$_.override bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$Type) && other is $JniPlugin$Type; + return other.runtimeType == ($JniPlugin$Type$) && other is $JniPlugin$Type$; } } diff --git a/pkgs/jni/pubspec.yaml b/pkgs/jni/pubspec.yaml index dc5a8b8757..26fe9545fd 100644 --- a/pkgs/jni/pubspec.yaml +++ b/pkgs/jni/pubspec.yaml @@ -17,7 +17,7 @@ topics: environment: sdk: '>=3.3.0 <4.0.0' - flutter: '>=2.11.0' + flutter: '>=3.35.6' dependencies: args: ^2.5.0 @@ -31,6 +31,8 @@ dev_dependencies: dart_flutter_team_lints: ^3.5.2 ffigen: path: ../ffigen + jnigen: + path: ../jnigen logging: ^1.2.0 test: ^1.25.8 diff --git a/pkgs/jni/tool/generate_jni_bindings.dart b/pkgs/jni/tool/generate_jni_bindings.dart index 126232c7c2..bd334d92a4 100644 --- a/pkgs/jni/tool/generate_jni_bindings.dart +++ b/pkgs/jni/tool/generate_jni_bindings.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:io'; + import 'package:jnigen/jnigen.dart'; void main() { @@ -13,7 +15,8 @@ void main() { ), outputConfig: OutputConfig( dartConfig: DartCodeOutputConfig( - path: Uri.file('lib/src/plugin/generated_plugin.dart'), + path: Platform.script + .resolve('../lib/src/plugin/generated_plugin.dart'), structure: OutputStructure.singleFile, ), ), From c92e2d1a640072c720901348853aca123b8bc83c Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 17 Oct 2025 11:39:41 +0200 Subject: [PATCH 06/12] Fix dartjni.c rebase --- .../com/github/dart_lang/jni/JniPlugin.java | 6 ++-- pkgs/jni/src/dartjni.c | 29 ------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java index a0cf533f25..b434164b34 100644 --- a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java +++ b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java @@ -10,9 +10,9 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class JniPlugin implements FlutterPlugin, ActivityAware { @@ -21,7 +21,7 @@ public class JniPlugin implements FlutterPlugin, ActivityAware { private long engineId; private Context context; private Activity activity; - private final List activityListeners = new ArrayList<>(); + private final Set activityListeners = new HashSet<>(); public static @NonNull Context getApplicationContext(long engineId) { return Objects.requireNonNull(pluginMap.get(engineId)).context; diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index 67893f9dcf..e221ce45aa 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -119,35 +119,6 @@ jobject GetClassLoader() { return (*jniEnv)->NewGlobalRef(jniEnv, jni_context.classLoader); } -#if defined(_WIN32) -// Pre-initialization of critical section on windows - this is required because -// there's no coordination between multiple isolates calling Spawn. -// -// Taken from https://stackoverflow.com/a/12858955 -CRITICAL_SECTION spawnLock = {0}; -BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module - DWORD fdwReason, // reason for calling function - LPVOID lpReserved) { // reserved - switch (fdwReason) { - case DLL_PROCESS_ATTACH: - // Initialize once for each new process. - // Return FALSE to fail DLL load. - InitializeCriticalSection(&spawnLock); - break; - case DLL_THREAD_DETACH: - if (jniEnv != NULL) { - detach_thread(jniEnv); - } - break; - case DLL_PROCESS_DETACH: - // Perform any necessary cleanup. - DeleteCriticalSection(&spawnLock); - break; - } - return TRUE; // Successful DLL_PROCESS_ATTACH. -} -#endif - // JNI Initialization #if defined(__ANDROID__) JNIEXPORT void JNICALL From 904a4c9bb7dd9c4cf372561891a41f276a03eab0 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 17 Oct 2025 11:43:17 +0200 Subject: [PATCH 07/12] Fix dartjni.c rebase --- pkgs/jni/src/dartjni.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index e221ce45aa..eb91a8c0bc 100644 --- a/pkgs/jni/src/dartjni.c +++ b/pkgs/jni/src/dartjni.c @@ -151,8 +151,6 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { } return TRUE; } -#else -pthread_mutex_t spawnLock = PTHREAD_MUTEX_INITIALIZER; #endif FFI_PLUGIN_EXPORT JniErrorCode SpawnJvm(JavaVMInitArgs* initArgs) { From 1d1fce2ea86d6ef39db65fb49c0ba86949c34ba5 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 17 Oct 2025 12:53:38 +0200 Subject: [PATCH 08/12] Build the example project in CI before the regen test --- .github/workflows/jnigen.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 6186997794..53dd63c5b1 100644 --- a/.github/workflows/jnigen.yaml +++ b/.github/workflows/jnigen.yaml @@ -221,6 +221,10 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} parallel: true path-to-lcov: ./pkgs/jni/coverage/lcov.info + - name: building the example project succeeds + working-directory: ./pkgs/jni/example/ + run: | + flutter build apk - name: regenerate & compare jnigen bindings run: | dart run tool/generate_jni_bindings.dart From 1c04bd495bc8548229828b8ac5aca87d9820a564 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Fri, 17 Oct 2025 13:19:58 +0200 Subject: [PATCH 09/12] Regen --- pkgs/jni/lib/src/plugin/generated_plugin.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/jni/lib/src/plugin/generated_plugin.dart b/pkgs/jni/lib/src/plugin/generated_plugin.dart index 871b77f285..0ae112ca48 100644 --- a/pkgs/jni/lib/src/plugin/generated_plugin.dart +++ b/pkgs/jni/lib/src/plugin/generated_plugin.dart @@ -35,8 +35,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' as core$_; import 'dart:core' show Object, String, bool, double, int; +import 'dart:core' as core$_; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; From fe6bfc11963420fa485090ec09792a30d9101e60 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Mon, 3 Nov 2025 18:02:25 +0100 Subject: [PATCH 10/12] Document limitations of androidActivity --- pkgs/jni/CHANGELOG.md | 3 +- .../com/github/dart_lang/jni/JniPlugin.java | 31 +- pkgs/jni/example/lib/main.dart | 93 +++--- pkgs/jni/lib/src/jni.dart | 87 +++-- pkgs/jni/lib/src/plugin/generated_plugin.dart | 302 +----------------- pkgs/jnigen/CHANGELOG.md | 3 +- .../in_app_java/lib/android_utils.g.dart | 2 +- .../kotlin_plugin/lib/kotlin_bindings.dart | 2 +- .../lib/notifications.dart | 2 +- .../org/apache/pdfbox/pdmodel/PDDocument.dart | 2 +- .../pdfbox/pdmodel/PDDocumentInformation.dart | 2 +- .../apache/pdfbox/text/PDFTextStripper.dart | 2 +- .../lib/src/bindings/dart_generator.dart | 2 +- .../fasterxml/jackson/core/JsonFactory.dart | 2 +- .../fasterxml/jackson/core/JsonParser.dart | 2 +- .../com/fasterxml/jackson/core/JsonToken.dart | 2 +- .../test/kotlin_test/bindings/kotlin.dart | 2 +- .../bindings/simple_package.dart | 2 +- pkgs/jnigen/tool/regenerate_all_bindings.dart | 2 +- 19 files changed, 142 insertions(+), 403 deletions(-) diff --git a/pkgs/jni/CHANGELOG.md b/pkgs/jni/CHANGELOG.md index 9e15590f39..a2f619b00c 100644 --- a/pkgs/jni/CHANGELOG.md +++ b/pkgs/jni/CHANGELOG.md @@ -6,8 +6,7 @@ - **Breaking Change**: Removed `Jni.getApplicationClassLoader()`, `Jni.getCurrentActivity()`, and `Jni.getCachedApplicationContext()`. Instead use `Jni.androidApplicationContext(engineId)` to access the application - context and listen to `Jni.androidActivities(engineId)` to acccess the - activity. + context and use `Jni.androidActivity(engineId)` to acccess the activity. - Update to the latest lints. ## 0.14.2 diff --git a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java index b434164b34..e27b883898 100644 --- a/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java +++ b/pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java @@ -7,42 +7,30 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class JniPlugin implements FlutterPlugin, ActivityAware { private static final ConcurrentHashMap pluginMap = new ConcurrentHashMap<>(); private long engineId; - private Context context; - private Activity activity; - private final Set activityListeners = new HashSet<>(); + private volatile Context context; + private volatile Activity activity; public static @NonNull Context getApplicationContext(long engineId) { return Objects.requireNonNull(pluginMap.get(engineId)).context; } - public interface ActivityListener { - void onActivityChanged(Activity activity); - } - - public static void addActivityListener(long engineId, @NonNull ActivityListener listener) { - var plugin = Objects.requireNonNull(pluginMap.get(engineId)); - plugin.activityListeners.add(listener); - listener.onActivityChanged(plugin.activity); - } - - public static void removeActivityListener(long engineId, @NonNull ActivityListener listener) { - var plugin = Objects.requireNonNull(pluginMap.get(engineId)); - plugin.activityListeners.remove(listener); + public static @Nullable Activity getActivity(long engineId) { + return Objects.requireNonNull(pluginMap.get(engineId)).activity; } @Override + @SuppressWarnings("deprecation") public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { //noinspection deprecation engineId = binding.getFlutterEngine().getEngineId(); @@ -57,15 +45,8 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { pluginMap.remove(engineId); } - private void notifyActivityListeners() { - for (var listener : activityListeners) { - listener.onActivityChanged(activity); - } - } - private void setActivity(Activity newActivity) { activity = newActivity; - notifyActivityListeners(); } @Override diff --git a/pkgs/jni/example/lib/main.dart b/pkgs/jni/example/lib/main.dart index 32e83ee092..a159a43620 100644 --- a/pkgs/jni/example/lib/main.dart +++ b/pkgs/jni/example/lib/main.dart @@ -32,20 +32,18 @@ String backAndForth() { return dartString; } -void quit(JObject activity) { +void quit() { + final activity = Jni.androidActivity(PlatformDispatcher.instance.engineId!); + if (activity == null) return; activity.jClass .instanceMethodId("finish", "()V") .call(activity, jvoid.type, []); + activity.release(); } -void showToast(String text, JObject activity) { - // This is example for calling your app's custom java code. - // Place the Toaster class in the app's android/ source Folder, with a Keep - // annotation or appropriate proguard rules to retain classes in release mode. - // - // In this example, Toaster class wraps android.widget.Toast so that it - // can be called from any thread. See - // android/app/src/main/java/com/github/dart_lang/jni_example/Toaster.java +void showToast(String text) { + final activity = Jni.androidActivity(PlatformDispatcher.instance.engineId!); + if (activity == null) return; final toasterClass = JClass.forName('com/github/dart_lang/jni_example/Toaster'); final makeText = toasterClass.staticMethodId( @@ -53,14 +51,20 @@ void showToast(String text, JObject activity) { '(Landroid/app/Activity;Landroid/content/Context;' 'Ljava/lang/CharSequence;I)' 'Lcom/github/dart_lang/jni_example/Toaster;'); + final applicationContext = + Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!); final toaster = makeText(toasterClass, JObject.type, [ activity, - Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!), - '😀'.toJString(), + applicationContext, + text.toJString(), 0, ]); final show = toasterClass.instanceMethodId('show', '()V'); show(toaster, jvoid.type, []); + toaster.release(); + applicationContext.release(); + activity.release(); + text.toJString().release(); } void main() { @@ -69,12 +73,12 @@ void main() { Jni.spawn(); } final examples = [ - Example("Math.random()", (_) => randomDouble(), runInitially: false), + Example("Math.random()", () => randomDouble(), runInitially: false), if (Platform.isAndroid) ...[ Example("Minutes of usage since reboot", - (_) => (uptime() / (60 * 1000)).floor()), - Example("Back and forth string conversion", (_) => backAndForth()), - Example("Device name", (_) { + () => (uptime() / (60 * 1000)).floor()), + Example("Back and forth string conversion", () => backAndForth()), + Example("Device name", () { final buildClass = JClass.forName("android/os/Build"); return buildClass .staticFieldId("DEVICE", JString.type.signature) @@ -83,13 +87,20 @@ void main() { }), Example( "Package name", - (activity) => activity.jClass - .instanceMethodId("getPackageName", "()Ljava/lang/String;") - .call(activity, JString.type, []), + () { + final activity = + Jni.androidActivity(PlatformDispatcher.instance.engineId!); + if (activity == null) return "Activity not available"; + final packageName = activity.jClass + .instanceMethodId("getPackageName", "()Ljava/lang/String;") + .call(activity, JString.type, []); + activity.release(); + return packageName; + }, ), Example( "Show toast", - (activity) => showToast("Hello from JNI!", activity), + () => showToast("Hello from JNI!"), runInitially: false, ), Example( @@ -104,29 +115,15 @@ void main() { class Example { String title; - dynamic Function(JObject activity) callback; + dynamic Function() callback; bool runInitially; Example(this.title, this.callback, {this.runInitially = true}); } -class MyApp extends StatefulWidget { +class MyApp extends StatelessWidget { const MyApp(this.examples, {super.key}); final List examples; - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - late Stream activityStream; - - @override - void initState() { - activityStream = - Jni.androidActivities(PlatformDispatcher.instance.engineId!); - super.initState(); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -134,29 +131,21 @@ class _MyAppState extends State { appBar: AppBar( title: const Text('JNI Examples'), ), - body: StreamBuilder( - stream: activityStream, - builder: (_, snapshot) { - if (!snapshot.hasData || snapshot.data == null) { - return Container(); - } - return ListView.builder( - itemCount: widget.examples.length, - itemBuilder: (context, i) { - final eg = widget.examples[i]; - return ExampleCard(eg, snapshot.data!); - }, - ); - }), + body: ListView.builder( + itemCount: examples.length, + itemBuilder: (context, i) { + final eg = examples[i]; + return ExampleCard(eg); + }, + ), ), ); } } class ExampleCard extends StatefulWidget { - const ExampleCard(this.example, this.activity, {super.key}); + const ExampleCard(this.example, {super.key}); final Example example; - final JObject activity; @override _ExampleCardState createState() => _ExampleCardState(); @@ -183,7 +172,7 @@ class _ExampleCardState extends State { var hasError = false; if (_run) { try { - result = eg.callback(widget.activity).toString(); + result = eg.callback().toString(); } on Exception catch (e) { hasError = true; result = e.toString(); diff --git a/pkgs/jni/lib/src/jni.dart b/pkgs/jni/lib/src/jni.dart index a6ef801a32..6bed9c2d91 100644 --- a/pkgs/jni/lib/src/jni.dart +++ b/pkgs/jni/lib/src/jni.dart @@ -2,7 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; @@ -214,36 +213,76 @@ abstract final class Jni { @internal static final env = GlobalJniEnv(_fetchGlobalEnv()); - /// Android application context. + /// Retrieves the global Android `ApplicationContext` associated with a + /// Flutter engine. /// /// Pass `PlatformDispatcher.instance.engineId` to the [engineId] field. + /// + /// The `ApplicationContext` is a long-lived singleton tied to the + /// application's lifecycle. It is safe to store and use from any thread + /// for non-UI tasks. static JObject androidApplicationContext(int engineId) { return JniPlugin.getApplicationContext(engineId); } - /// A stream of Android activities. + /// Retrieves the current Android `Activity` associated with a Flutter engine. /// - /// Pass `PlatformDispatcher.instance.engineId` to the [engineId] field. - static Stream androidActivities(int engineId) { - late final StreamController androidActivitiesController; - final activityListener = JniPlugin$ActivityListener.implement( - $JniPlugin$ActivityListener( - onActivityChanged$async: true, - onActivityChanged: (activity) { - androidActivitiesController.add(activity); - }, - ), - ); - androidActivitiesController = StreamController( - onListen: () { - JniPlugin.addActivityListener(engineId, activityListener); - }, - onCancel: () { - JniPlugin.removeActivityListener(engineId, activityListener); - activityListener.release(); - }, - ); - return androidActivitiesController.stream; + /// The `engineId` can be obtained from `PlatformDispatcher.instance.engineId` + /// in Dart. + /// + /// **WARNING: This reference is volatile and must be used with care.** + /// + /// The Android `Activity` lifecycle is asynchronous. The `Activity` returned + /// by this function can become `null` or stale (destroyed) at any moment, + /// such as during screen rotation or when the app is backgrounded. + /// + /// To prevent native crashes, this function has two strict usage rules: + /// + /// 1. **Platform Thread Only**: It must *only* be called from the platform + /// thread. + /// 2. **Synchronous Use Only**: The returned `JObject` must be used + /// immediately and synchronously, with no asynchronous gaps (`await`). + /// + /// Do not store the returned `JObject` in a field or local variable that + /// persists across an `await`. + /// + /// --- + /// + /// ### Correct Usage (Synchronous, "Get-and-Use"): + /// + /// ```dart + /// void safeCall() { + /// // This is safe because the `Activity` is retrieved and used + /// // in a single, unbroken, synchronous block. + /// final activity = Jni.androidActivity(engineId); + /// if (activity != null) { + /// someGeneratedApi.doSomething(activity); + /// activity.release(); + /// } + /// } + /// ``` + /// + /// ### **DANGEROUS** Usage (Asynchronous Gap): + /// + /// ```dart + /// Future dangerousCall() async { + /// // 1. Get the Activity (e.g., Activity "A") + /// final activity = Jni.androidActivity(engineId); + /// + /// // 2. An `await` occurs. The main thread is freed. + /// // While waiting, Android might destroy Activity "A" and create "B". + /// await someOtherFuture(); + /// + /// // 3. CRASH: The code resumes, but `activity` is now a stale + /// // reference to the destroyed Activity "A". + /// if (activity != null) { + /// someGeneratedApi.doSomething(activity); // This will crash + /// activity.release(); + /// } + /// } + /// ``` + static JObject? androidActivity(int engineId) { + return JniPlugin.getActivity(engineId); } } diff --git a/pkgs/jni/lib/src/plugin/generated_plugin.dart b/pkgs/jni/lib/src/plugin/generated_plugin.dart index 0ae112ca48..cc4b90198c 100644 --- a/pkgs/jni/lib/src/plugin/generated_plugin.dart +++ b/pkgs/jni/lib/src/plugin/generated_plugin.dart @@ -35,244 +35,12 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; -/// from: `com.github.dart_lang.jni.JniPlugin$ActivityListener` -class JniPlugin$ActivityListener extends jni$_.JObject { - @jni$_.internal - @core$_.override - final jni$_.JType $type; - - @jni$_.internal - JniPlugin$ActivityListener.fromReference( - jni$_.JReference reference, - ) : $type = type, - super.fromReference(reference); - - static final _class = jni$_.JClass.forName( - r'com/github/dart_lang/jni/JniPlugin$ActivityListener'); - - /// The type which includes information such as the signature of this class. - static const jni$_.JType nullableType = - $JniPlugin$ActivityListener$NullableType$(); - - /// The type which includes information such as the signature of this class. - static const jni$_.JType type = - $JniPlugin$ActivityListener$Type$(); - static final _id_onActivityChanged = _class.instanceMethodId( - r'onActivityChanged', - r'(Landroid/app/Activity;)V', - ); - - static final _onActivityChanged = 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 abstract void onActivityChanged(android.app.Activity activity)` - void onActivityChanged( - jni$_.JObject? activity, - ) { - final _$activity = activity?.reference ?? jni$_.jNullReference; - _onActivityChanged(reference.pointer, - _id_onActivityChanged as jni$_.JMethodIDPtr, _$activity.pointer) - .check(); - } - - /// Maps a specific port to the implemented interface. - static final core$_.Map _$impls = {}; - static jni$_.JObjectPtr _$invoke( - 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( - int $p, - jni$_.MethodInvocation $i, - ) { - try { - final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); - final $a = $i.args; - if ($d == r'onActivityChanged(Landroid/app/Activity;)V') { - _$impls[$p]!.onActivityChanged( - $a![0]?.as(const jni$_.$JObject$Type$(), releaseOriginal: true), - ); - return jni$_.nullptr; - } - } catch (e) { - return jni$_.ProtectedJniExtensions.newDartException(e); - } - return jni$_.nullptr; - } - - static void implementIn( - jni$_.JImplementer implementer, - $JniPlugin$ActivityListener $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.github.dart_lang.jni.JniPlugin$ActivityListener', - $p, - _$invokePointer, - [ - if ($impl.onActivityChanged$async) - r'onActivityChanged(Landroid/app/Activity;)V', - ], - ); - final $a = $p.sendPort.nativePort; - _$impls[$a] = $impl; - } - - factory JniPlugin$ActivityListener.implement( - $JniPlugin$ActivityListener $impl, - ) { - final $i = jni$_.JImplementer(); - implementIn($i, $impl); - return JniPlugin$ActivityListener.fromReference( - $i.implementReference(), - ); - } -} - -abstract base mixin class $JniPlugin$ActivityListener { - factory $JniPlugin$ActivityListener({ - required void Function(jni$_.JObject? activity) onActivityChanged, - bool onActivityChanged$async, - }) = _$JniPlugin$ActivityListener; - - void onActivityChanged(jni$_.JObject? activity); - bool get onActivityChanged$async => false; -} - -final class _$JniPlugin$ActivityListener with $JniPlugin$ActivityListener { - _$JniPlugin$ActivityListener({ - required void Function(jni$_.JObject? activity) onActivityChanged, - this.onActivityChanged$async = false, - }) : _onActivityChanged = onActivityChanged; - - final void Function(jni$_.JObject? activity) _onActivityChanged; - final bool onActivityChanged$async; - - void onActivityChanged(jni$_.JObject? activity) { - return _onActivityChanged(activity); - } -} - -final class $JniPlugin$ActivityListener$NullableType$ - extends jni$_.JType { - @jni$_.internal - const $JniPlugin$ActivityListener$NullableType$(); - - @jni$_.internal - @core$_.override - String get signature => - r'Lcom/github/dart_lang/jni/JniPlugin$ActivityListener;'; - - @jni$_.internal - @core$_.override - JniPlugin$ActivityListener? fromReference(jni$_.JReference reference) => - reference.isNull - ? null - : JniPlugin$ActivityListener.fromReference( - reference, - ); - @jni$_.internal - @core$_.override - jni$_.JType get superType => const jni$_.$JObject$NullableType$(); - - @jni$_.internal - @core$_.override - jni$_.JType get nullableType => this; - - @jni$_.internal - @core$_.override - final superCount = 1; - - @core$_.override - int get hashCode => ($JniPlugin$ActivityListener$NullableType$).hashCode; - - @core$_.override - bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$ActivityListener$NullableType$) && - other is $JniPlugin$ActivityListener$NullableType$; - } -} - -final class $JniPlugin$ActivityListener$Type$ - extends jni$_.JType { - @jni$_.internal - const $JniPlugin$ActivityListener$Type$(); - - @jni$_.internal - @core$_.override - String get signature => - r'Lcom/github/dart_lang/jni/JniPlugin$ActivityListener;'; - - @jni$_.internal - @core$_.override - JniPlugin$ActivityListener fromReference(jni$_.JReference reference) => - JniPlugin$ActivityListener.fromReference( - reference, - ); - @jni$_.internal - @core$_.override - jni$_.JType get superType => const jni$_.$JObject$NullableType$(); - - @jni$_.internal - @core$_.override - jni$_.JType get nullableType => - const $JniPlugin$ActivityListener$NullableType$(); - - @jni$_.internal - @core$_.override - final superCount = 1; - - @core$_.override - int get hashCode => ($JniPlugin$ActivityListener$Type$).hashCode; - - @core$_.override - bool operator ==(Object other) { - return other.runtimeType == ($JniPlugin$ActivityListener$Type$) && - other is $JniPlugin$ActivityListener$Type$; - } -} - /// from: `com.github.dart_lang.jni.JniPlugin` class JniPlugin extends jni$_.JObject { @jni$_.internal @@ -342,66 +110,28 @@ class JniPlugin extends jni$_.JObject { .object(const jni$_.$JObject$Type$()); } - static final _id_addActivityListener = _class.staticMethodId( - r'addActivityListener', - r'(JLcom/github/dart_lang/jni/JniPlugin$ActivityListener;)V', - ); - - static final _addActivityListener = jni$_.ProtectedJniExtensions.lookup< - jni$_.NativeFunction< - jni$_.JThrowablePtr Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_ - .VarArgs<(jni$_.Int64, jni$_.Pointer)>)>>( - 'globalEnv_CallStaticVoidMethod') - .asFunction< - jni$_.JThrowablePtr Function(jni$_.Pointer, - jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); - - /// from: `static public void addActivityListener(long j, com.github.dart_lang.jni.JniPlugin$ActivityListener activityListener)` - static void addActivityListener( - int j, - JniPlugin$ActivityListener activityListener, - ) { - final _$activityListener = activityListener.reference; - _addActivityListener( - _class.reference.pointer, - _id_addActivityListener as jni$_.JMethodIDPtr, - j, - _$activityListener.pointer) - .check(); - } - - static final _id_removeActivityListener = _class.staticMethodId( - r'removeActivityListener', - r'(JLcom/github/dart_lang/jni/JniPlugin$ActivityListener;)V', + static final _id_getActivity = _class.staticMethodId( + r'getActivity', + r'(J)Landroid/app/Activity;', ); - static final _removeActivityListener = jni$_.ProtectedJniExtensions.lookup< + static final _getActivity = jni$_.ProtectedJniExtensions.lookup< jni$_.NativeFunction< - jni$_.JThrowablePtr Function( - jni$_.Pointer, - jni$_.JMethodIDPtr, - jni$_ - .VarArgs<(jni$_.Int64, jni$_.Pointer)>)>>( - 'globalEnv_CallStaticVoidMethod') + jni$_.JniResult Function(jni$_.Pointer, + jni$_.JMethodIDPtr, jni$_.VarArgs<(jni$_.Int64,)>)>>( + 'globalEnv_CallStaticObjectMethod') .asFunction< - jni$_.JThrowablePtr Function(jni$_.Pointer, - jni$_.JMethodIDPtr, int, jni$_.Pointer)>(); + jni$_.JniResult Function( + jni$_.Pointer, jni$_.JMethodIDPtr, int)>(); - /// from: `static public void removeActivityListener(long j, com.github.dart_lang.jni.JniPlugin$ActivityListener activityListener)` - static void removeActivityListener( + /// from: `static public android.app.Activity getActivity(long j)` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JObject? getActivity( int j, - JniPlugin$ActivityListener activityListener, ) { - final _$activityListener = activityListener.reference; - _removeActivityListener( - _class.reference.pointer, - _id_removeActivityListener as jni$_.JMethodIDPtr, - j, - _$activityListener.pointer) - .check(); + return _getActivity( + _class.reference.pointer, _id_getActivity as jni$_.JMethodIDPtr, j) + .object(const jni$_.$JObject$NullableType$()); } static final _id_onAttachedToEngine = _class.instanceMethodId( diff --git a/pkgs/jnigen/CHANGELOG.md b/pkgs/jnigen/CHANGELOG.md index 8188beb767..4bb2fd6383 100644 --- a/pkgs/jnigen/CHANGELOG.md +++ b/pkgs/jnigen/CHANGELOG.md @@ -2,7 +2,8 @@ - Changed the names of internal type classes. - **Breaking Change**: Removed `exclude` from config. -- Update to the latest lints. +- Updated to the latest lints. +- Sorted imports. ## 0.14.2 diff --git a/pkgs/jnigen/example/in_app_java/lib/android_utils.g.dart b/pkgs/jnigen/example/in_app_java/lib/android_utils.g.dart index 949ae5c357..1f86a3b535 100644 --- a/pkgs/jnigen/example/in_app_java/lib/android_utils.g.dart +++ b/pkgs/jnigen/example/in_app_java/lib/android_utils.g.dart @@ -30,8 +30,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart index d3c49bb664..a6b68485a1 100644 --- a/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart +++ b/pkgs/jnigen/example/kotlin_plugin/lib/kotlin_bindings.dart @@ -30,8 +30,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/example/notification_plugin/lib/notifications.dart b/pkgs/jnigen/example/notification_plugin/lib/notifications.dart index c2a2e039c1..c1fd0c4dbb 100644 --- a/pkgs/jnigen/example/notification_plugin/lib/notifications.dart +++ b/pkgs/jnigen/example/notification_plugin/lib/notifications.dart @@ -34,8 +34,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart index fb2478c9e2..3b468b245e 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocument.dart @@ -48,8 +48,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart index a563fa219c..46fd071541 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/pdmodel/PDDocumentInformation.dart @@ -48,8 +48,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart index e33a8f97b4..1ae3c990fc 100644 --- a/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart +++ b/pkgs/jnigen/example/pdfbox_plugin/lib/src/third_party/org/apache/pdfbox/text/PDFTextStripper.dart @@ -48,8 +48,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart index bee93d0dd1..d92ed11dd3 100644 --- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart +++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart @@ -144,8 +144,8 @@ class DartGenerator extends Visitor> { static const autoGeneratedNotice = '// AUTO GENERATED BY JNIGEN $version. ' 'DO NOT EDIT!\n'; static const defaultImports = ''' -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as $_core; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as $_jni; import 'package:jni/jni.dart' as $_jni; diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart index 5f1c056513..6f5c1cb6fa 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonFactory.dart @@ -47,8 +47,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart index b890712bd3..f8ed9e7036 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonParser.dart @@ -47,8 +47,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart index 7d327bfd10..aa075dc602 100644 --- a/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart +++ b/pkgs/jnigen/test/jackson_core_test/third_party/bindings/com/fasterxml/jackson/core/JsonToken.dart @@ -47,8 +47,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart index d929299b14..2a14012ab0 100644 --- a/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart +++ b/pkgs/jnigen/test/kotlin_test/bindings/kotlin.dart @@ -34,8 +34,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart index 7969db39a4..e41c179208 100644 --- a/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart +++ b/pkgs/jnigen/test/simple_package_test/bindings/simple_package.dart @@ -34,8 +34,8 @@ // ignore_for_file: unused_shown_name // ignore_for_file: use_super_parameters -import 'dart:core' show Object, String, bool, double, int; import 'dart:core' as core$_; +import 'dart:core' show Object, String, bool, double, int; import 'package:jni/_internal.dart' as jni$_; import 'package:jni/jni.dart' as jni$_; diff --git a/pkgs/jnigen/tool/regenerate_all_bindings.dart b/pkgs/jnigen/tool/regenerate_all_bindings.dart index a19c7a0f02..05be28daba 100644 --- a/pkgs/jnigen/tool/regenerate_all_bindings.dart +++ b/pkgs/jnigen/tool/regenerate_all_bindings.dart @@ -14,10 +14,10 @@ const scripts = [ 'test/jackson_core_test/generate.dart', 'test/simple_package_test/generate.dart', 'test/kotlin_test/generate.dart', + 'example/in_app_java/tool/jnigen.dart', ]; const yamlBasedExamples = [ - 'example/in_app_java', 'example/pdfbox_plugin', 'example/notification_plugin', 'example/kotlin_plugin', From fb7178577306e84199a5b57e31e78187f0913bd1 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Wed, 5 Nov 2025 17:43:58 +0100 Subject: [PATCH 11/12] Fix jnigen examples --- pkgs/jnigen/example/in_app_java/lib/main.dart | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/pkgs/jnigen/example/in_app_java/lib/main.dart b/pkgs/jnigen/example/in_app_java/lib/main.dart index adb28abffa..f06354dfc7 100644 --- a/pkgs/jnigen/example/in_app_java/lib/main.dart +++ b/pkgs/jnigen/example/in_app_java/lib/main.dart @@ -2,8 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:ui'; - +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jni/jni.dart'; @@ -11,9 +10,8 @@ import 'package:jni/jni.dart'; // structure. import 'android_utils.g.dart'; -JObject context = Jni.androidApplicationContext( - PlatformDispatcher.instance.engineId!, -); +JObject context = + Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!); final hashmap = HashMap(K: JString.type, V: JString.type); @@ -29,7 +27,7 @@ const sunglassEmoji = "😎"; /// Display device model number and the number of times this was called /// as Toast. -void showToast(JObject activity) { +void showToast() { final toastCount = hashmap.getOrDefault( "toastCount".toJString(), 0.toJString(), @@ -41,7 +39,11 @@ void showToast(JObject activity) { : ':cool:'; final message = '${newToastCount.toDartString()} - ${Build.MODEL!.toDartString()} $emoji'; - AndroidUtils.showToast(activity, message.toJString(), 0); + AndroidUtils.showToast( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + message.toJString(), + 0, + ); } void main() { @@ -62,50 +64,26 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatefulWidget { +class MyHomePage extends StatelessWidget { const MyHomePage({super.key, required this.title}); final String title; - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - late final Stream activityStream; - - @override - void initState() { - activityStream = Jni.androidActivities( - PlatformDispatcher.instance.engineId!, - ); - super.initState(); - } - @override Widget build(BuildContext context) { - return StreamBuilder( - stream: activityStream, - builder: (context, asyncSnapshot) { - if (!asyncSnapshot.hasData || asyncSnapshot.data == null) { - return Container(); - } - final activity = asyncSnapshot.data!; - return Scaffold( - appBar: AppBar(title: Text(widget.title)), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - child: const Text('Show Device Model'), - onPressed: () => showToast(activity), - ), - ], + return Scaffold( + appBar: AppBar(title: Text(title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + child: const Text('Show Device Model'), + onPressed: () => showToast(), ), - ), - ); - }, + ], + ), + ), ); } } From 5ff04fbc2cce1700d1bde2f7313bec3caeb55863 Mon Sep 17 00:00:00 2001 From: Hossein Yousefi Date: Wed, 5 Nov 2025 18:04:32 +0100 Subject: [PATCH 12/12] Fix jnigen examples --- .../notification_plugin/example/lib/main.dart | 100 +++++++----------- 1 file changed, 39 insertions(+), 61 deletions(-) diff --git a/pkgs/jnigen/example/notification_plugin/example/lib/main.dart b/pkgs/jnigen/example/notification_plugin/example/lib/main.dart index 11c836ff63..796db1e96a 100644 --- a/pkgs/jnigen/example/notification_plugin/example/lib/main.dart +++ b/pkgs/jnigen/example/notification_plugin/example/lib/main.dart @@ -12,11 +12,16 @@ import 'package:notification_plugin/notifications.dart'; int i = 0; -void showNotification(String title, String text, JObject activity) { +void showNotification(String title, String text) { i = i + 1; var jTitle = JString.fromString(title); var jText = JString.fromString(text); - Notifications.showNotification(activity, i, jTitle, jText); + Notifications.showNotification( + Jni.androidActivity(PlatformDispatcher.instance.engineId!), + i, + jTitle, + jText, + ); jTitle.release(); jText.release(); } @@ -40,75 +45,48 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); +class MyHomePage extends StatelessWidget { + MyHomePage({super.key, required this.title}); final String title; - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { final _title = TextEditingController(text: 'Hello from JNI'); final _text = TextEditingController(text: '😀'); - final activityStream = - Jni.androidActivities(PlatformDispatcher.instance.engineId!); - - @override - void dispose() { - _title.dispose(); - _text.dispose(); - super.dispose(); - } @override Widget build(BuildContext context) { - return StreamBuilder( - stream: activityStream, - builder: (context, asyncSnapshot) { - if (!asyncSnapshot.hasData || asyncSnapshot.data == null) { - return Container(); - } - final activity = asyncSnapshot.data!; - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: _title, - textCapitalization: TextCapitalization.sentences, - decoration: - const InputDecoration(labelText: 'Notification title'), - ), - TextFormField( - controller: _text, - keyboardType: TextInputType.multiline, - minLines: 1, - maxLines: 4, - decoration: - const InputDecoration(labelText: 'Notification text'), - ), - ElevatedButton( - child: const Text('Show Notification'), - onPressed: () => showNotification( - _title.text, - _text.text, - activity, - ), - ), - ], + return Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: _title, + textCapitalization: TextCapitalization.sentences, + decoration: + const InputDecoration(labelText: 'Notification title'), + ), + TextFormField( + controller: _text, + keyboardType: TextInputType.multiline, + minLines: 1, + maxLines: 4, + decoration: + const InputDecoration(labelText: 'Notification text'), ), - ), + ElevatedButton( + child: const Text('Show Notification'), + onPressed: () => showNotification(_title.text, _text.text), + ), + ], ), - ); - }, + ), + ), ); } }