diff --git a/.github/workflows/jnigen.yaml b/.github/workflows/jnigen.yaml index 2b174beeba..b2619b1c3e 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 - 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 . @@ -318,10 +315,6 @@ jobs: working-directory: ./pkgs/jnigen steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - - name: Setup clang format - uses: ConorMacBride/install-package@3e7ad059e07782ee54fa35f827df52aae0626f30 - with: - brew: clang-format - uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 with: channel: 'stable' @@ -419,7 +412,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 diff --git a/pkgs/jni/CHANGELOG.md b/pkgs/jni/CHANGELOG.md index 655221f1d7..6d8cb30050 100644 --- a/pkgs/jni/CHANGELOG.md +++ b/pkgs/jni/CHANGELOG.md @@ -1,5 +1,10 @@ -## 0.14.3-wip +## 0.15.0-wip +- **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. - Update to the latest lints. ## 0.14.2 @@ -10,12 +15,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 8d86367104..cdcea0240f 100644 --- a/pkgs/jni/example/lib/main.dart +++ b/pkgs/jni/example/lib/main.dart @@ -6,6 +6,7 @@ import 'dart:ffi'; import 'dart:io'; +import 'dart:ui'; import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; @@ -73,12 +74,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. @@ -93,9 +95,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, ]); @@ -104,19 +106,20 @@ void showToast(String text) { } void main() { + WidgetsFlutterBinding.ensureInitialized(); if (!Platform.isAndroid) { Jni.spawn(); } final examples = [ - Example("String.valueOf(1332)", () => toJavaStringUsingEnv(1332)), - Example("Generate random number", () => randomUsingEnv(180), + Example("String.valueOf(1332)", (_) => toJavaStringUsingEnv(1332)), + Example("Generate random number", (_) => randomUsingEnv(180), runInitially: false), - 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) @@ -125,13 +128,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, @@ -144,7 +149,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}); } @@ -158,8 +163,12 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { + late Stream activityStream; + @override void initState() { + activityStream = + Jni.androidActivities(PlatformDispatcher.instance.engineId!); super.initState(); } @@ -170,11 +179,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!); + }, + ); }), ), ); @@ -182,8 +199,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(); @@ -210,7 +228,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 3cd06f60bc..5469d3a8d1 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'; @@ -211,22 +213,37 @@ abstract final class Jni { /// any thread, and always returns global object references. 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/pubspec.yaml b/pkgs/jni/pubspec.yaml index a4baaceb84..bdd93f336b 100644 --- a/pkgs/jni/pubspec.yaml +++ b/pkgs/jni/pubspec.yaml @@ -4,7 +4,7 @@ name: jni description: A library to access JNI from Dart and Flutter that acts as a support library for package:jnigen. -version: 0.14.3-wip +version: 0.15.0-wip repository: https://github.com/dart-lang/native/tree/main/pkgs/jni issue_tracker: https://github.com/dart-lang/native/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ajni @@ -31,6 +31,7 @@ dev_dependencies: dart_flutter_team_lints: ^3.5.2 ## Pin ffigen version because we are depending on internal APIs. ffigen: 16.1.0 + jnigen: 0.14.2 logging: ^1.2.0 test: ^1.25.8 @@ -50,3 +51,5 @@ flutter: dependency_overrides: ffigen: path: ../ffigen + jnigen: + path: ../jnigen diff --git a/pkgs/jni/src/dartjni.c b/pkgs/jni/src/dartjni.c index 28bfabc97c..f72bb0ffcb 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,57 +119,7 @@ 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__ -JNIEXPORT void JNICALL -Java_com_github_dart_1lang_jni_JniPlugin_initializeJni(JNIEnv* env, - jobject obj, - jobject appContext, - 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", - "(Ljava/lang/String;)Ljava/lang/Class;"); - 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 -#ifdef _WIN32 +#if defined(_WIN32) // Pre-initialization of critical section on windows - this is required because // there's no coordination between multiple isolates calling Spawn. // @@ -197,9 +146,27 @@ BOOL WINAPI DllMain(HINSTANCE hinstDLL, // handle to DLL module } return TRUE; // Successful DLL_PROCESS_ATTACH. } -#else -pthread_mutex_t spawnLock = PTHREAD_MUTEX_INITIALIZER; #endif + +// JNI Initialization +#if defined(__ANDROID__) +JNIEXPORT void JNICALL +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); + jclass classLoaderClass = (*env)->GetObjectClass(env, classLoader); + jni_context.loadClassMethod = + (*env)->GetMethodID(env, classLoaderClass, "loadClass", + "(Ljava/lang/String;)Ljava/lang/Class;"); + init(); +} + +// Sometimes you may get linker error trying to link JNI_CreateJavaVM APIs +// on Android NDK. So IFDEF is required. +#else FFI_PLUGIN_EXPORT JniErrorCode SpawnJvm(JavaVMInitArgs* initArgs) { if (jni_context.jvm != NULL) { @@ -477,9 +444,11 @@ Java_com_github_dart_1lang_jni_PortProxyBuilder__1invoke( if (mustEnterIsolate) { Dart_EnterIsolate_DL((Dart_Isolate)isolateId); } - result->object = ((jobject(*)(uint64_t, jobject, jobject))functionPtr)( - port, (*env)->NewGlobalRef(env, methodDescriptor), - (*env)->NewGlobalRef(env, args)); + typedef jobject (*JniFunction)(uint64_t p, jobject md, jobject a); + JniFunction func_to_call = (JniFunction)functionPtr; + result->object = + func_to_call(port, (*env)->NewGlobalRef(env, methodDescriptor), + (*env)->NewGlobalRef(env, args)); if (mustEnterIsolate) { Dart_ExitIsolate_DL(); } diff --git a/pkgs/jni/src/dartjni.h b/pkgs/jni/src/dartjni.h index 3139ee1945..a9a7a23474 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 CRITICAL_SECTION 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''', + ), + ); +} diff --git a/pkgs/jnigen/example/in_app_java/lib/main.dart b/pkgs/jnigen/example/in_app_java/lib/main.dart index 4dbdc84bcc..1c1f8fdb2f 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,12 +11,12 @@ import 'package:jni/jni.dart'; // structure. import 'android_utils.dart'; -JObject activity = JObject.fromReference(Jni.getCurrentActivity()); -JObject context = JObject.fromReference(Jni.getCachedApplicationContext()); +JObject context = + Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!); final hashmap = HashMap.new$2(K: JString.type, V: JString.type); -final emojiCompat = EmojiCompat.get()!; +final emojiCompat = EmojiCompat.get(); extension IntX on int { JString toJString() { @@ -26,9 +28,11 @@ const sunglassEmoji = "😎"; /// Display device model number and the number of times this was called /// as Toast. -void showToast() { - final toastCount = - hashmap.getOrDefault("toastCount".toJString(), 0.toJString()); +void showToast(JObject activity) { + final toastCount = hashmap.getOrDefault( + "toastCount".toJString(), + 0.toJString(), + ); final newToastCount = (int.parse(toastCount!.toDartString()) + 1).toJString(); hashmap.put("toastCount".toJString(), newToastCount); final emoji = emojiCompat.hasEmojiGlyph(sunglassEmoji.toJString()) @@ -51,36 +55,54 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.teal, - ), + theme: ThemeData(primarySwatch: Colors.teal), home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } -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