Skip to content

Commit f75c068

Browse files
[jni] Fix the context when using multiple flutter engines (#2494)
1 parent ad6ae81 commit f75c068

File tree

31 files changed

+634
-171
lines changed

31 files changed

+634
-171
lines changed

.github/workflows/jnigen.yaml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,6 @@ jobs:
9090
distribution: 'zulu'
9191
java-version: '17'
9292
cache: maven
93-
## Committed bindings are formatted with clang-format.
94-
## So this is required to format generated bindings identically
95-
- name: install clang tools
96-
run: |
97-
sudo apt-get update -y
98-
sudo apt-get install -y clang-format
9993
- name: Install dependencies
10094
run: dart pub get
10195
- name: build in_app_java APK
@@ -172,8 +166,11 @@ jobs:
172166
working-directory: ./pkgs/jni
173167
- name: install clang tools & CMake
174168
run: |
169+
wget https://apt.llvm.org/llvm.sh
170+
chmod +x llvm.sh
171+
sudo ./llvm.sh 20
175172
sudo apt-get update -y
176-
sudo apt-get install -y clang-format build-essential cmake
173+
sudo apt-get install -y clang-format-20 build-essential cmake
177174
- run: flutter pub get
178175
- name: Check formatting
179176
run: dart format --output=none --set-exit-if-changed .
@@ -224,6 +221,14 @@ jobs:
224221
github-token: ${{ secrets.GITHUB_TOKEN }}
225222
parallel: true
226223
path-to-lcov: ./pkgs/jni/coverage/lcov.info
224+
- name: building the example project succeeds
225+
working-directory: ./pkgs/jni/example/
226+
run: |
227+
flutter build apk
228+
- name: regenerate & compare jnigen bindings
229+
run: |
230+
dart run tool/generate_jni_bindings.dart
231+
git diff --exit-code -- lib/src/plugin
227232
# TODO(https://github.com/dart-lang/ffigen/issues/555): FFIgen generated
228233
# on my machine has macOS specific stuff and CI does not.
229234
# We should just generate the struct as opaque, but we currently can't.
@@ -419,7 +424,7 @@ jobs:
419424
java-version: '17'
420425
- run: |
421426
sudo apt-get update -y
422-
sudo apt-get install -y ninja-build libgtk-3-dev clang-format
427+
sudo apt-get install -y ninja-build libgtk-3-dev
423428
- run: flutter config --enable-linux-desktop
424429
- run: dart pub get
425430
- name: Generate full bindings

pkgs/jni/CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
- **Breaking Change**: Made `Jni.env` internal.
44
- **Breaking Change**: Renamed `JObjType` to `JType`.
55
- **Breaking Change**: Made all of the type classes internal.
6+
- **Breaking Change**: Removed `Jni.getApplicationClassLoader()`,
7+
`Jni.getCurrentActivity()`, and `Jni.getCachedApplicationContext()`. Instead
8+
use `Jni.androidApplicationContext(engineId)` to access the application
9+
context and use `Jni.androidActivity(engineId)` to acccess the activity.
610
- Update to the latest lints.
711

812
## 0.14.2
@@ -13,12 +17,13 @@
1317

1418
## 0.14.1
1519

16-
- Updated `bin/setup.dart` to use Gradle instead of Maven for building Java sources. Added gradle executables
17-
and bootstrap jars [#2003](https://github.com/dart-lang/native/issues/2003)
18-
- Added `JObject.isInstanceOf` which checks whether a `JObject` is an instance
20+
- Updated `bin/setup.dart` to use Gradle instead of Maven for building Java
21+
sources. Added gradle executables and bootstrap jars
22+
[#2003](https://github.com/dart-lang/native/issues/2003)
23+
- Added `JObject.isInstanceOf` which checks whether a `JObject` is an instance
1924
of a java class.
20-
- Fixed a [bug](https://github.com/dart-lang/native/issues/1908) where
21-
Java interfaces implemented in on the main thread in Dart could deadlock when
25+
- Fixed a [bug](https://github.com/dart-lang/native/issues/1908) where Java
26+
interfaces implemented in on the main thread in Dart could deadlock when
2227
invoked from the main thread outside the context of a Dart isolate.
2328

2429
## 0.14.0

pkgs/jni/android/src/main/java/com/github/dart_lang/jni/JniPlugin.java

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,72 @@
77
import android.app.Activity;
88
import android.content.Context;
99
import androidx.annotation.NonNull;
10+
import androidx.annotation.Nullable;
1011
import io.flutter.embedding.engine.plugins.FlutterPlugin;
1112
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
1213
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
14+
import java.util.Objects;
15+
import java.util.concurrent.ConcurrentHashMap;
1316

1417
public class JniPlugin implements FlutterPlugin, ActivityAware {
18+
private static final ConcurrentHashMap<Long, JniPlugin> pluginMap = new ConcurrentHashMap<>();
19+
20+
private long engineId;
21+
private volatile Context context;
22+
private volatile Activity activity;
23+
24+
public static @NonNull Context getApplicationContext(long engineId) {
25+
return Objects.requireNonNull(pluginMap.get(engineId)).context;
26+
}
27+
28+
public static @Nullable Activity getActivity(long engineId) {
29+
return Objects.requireNonNull(pluginMap.get(engineId)).activity;
30+
}
1531

1632
@Override
33+
@SuppressWarnings("deprecation")
1734
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
18-
setup(binding.getApplicationContext());
35+
//noinspection deprecation
36+
engineId = binding.getFlutterEngine().getEngineId();
37+
context = binding.getApplicationContext();
38+
pluginMap.put(engineId, this);
1939
}
2040

21-
private void setup(Context context) {
22-
initializeJni(context, getClass().getClassLoader());
41+
@Override
42+
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
43+
context = null;
44+
activity = null;
45+
pluginMap.remove(engineId);
2346
}
2447

25-
@Override
26-
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
48+
private void setActivity(Activity newActivity) {
49+
activity = newActivity;
50+
}
2751

28-
// Activity handling methods
2952
@Override
3053
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
31-
Activity activity = binding.getActivity();
32-
setJniActivity(activity, activity.getApplicationContext());
54+
setActivity(binding.getActivity());
3355
}
3456

3557
@Override
36-
public void onDetachedFromActivityForConfigChanges() {}
58+
public void onDetachedFromActivityForConfigChanges() {
59+
setActivity(null);
60+
}
3761

3862
@Override
3963
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
40-
Activity activity = binding.getActivity();
41-
setJniActivity(activity, activity.getApplicationContext());
64+
setActivity(binding.getActivity());
4265
}
4366

4467
@Override
45-
public void onDetachedFromActivity() {}
46-
47-
native void initializeJni(Context context, ClassLoader classLoader);
68+
public void onDetachedFromActivity() {
69+
setActivity(null);
70+
}
4871

49-
native void setJniActivity(Activity activity, Context context);
72+
static native void setClassLoader(ClassLoader classLoader);
5073

5174
static {
5275
System.loadLibrary("dartjni");
76+
setClassLoader(JniPlugin.class.getClassLoader());
5377
}
5478
}

pkgs/jni/example/android/settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pluginManagement {
1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
2121
id "com.android.application" version "8.6.0" apply false
22-
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
22+
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
2323
}
2424

2525
include ":app"

pkgs/jni/example/lib/main.dart

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// ignore_for_file: library_private_types_in_public_api
66

77
import 'dart:io';
8+
import 'dart:ui';
89

910
import 'package:flutter/material.dart';
1011
import 'package:jni/jni.dart';
@@ -32,36 +33,42 @@ String backAndForth() {
3233
}
3334

3435
void quit() {
35-
JObject.fromReference(Jni.getCurrentActivity()).use((ac) =>
36-
ac.jClass.instanceMethodId("finish", "()V").call(ac, jvoid.type, []));
36+
final activity = Jni.androidActivity(PlatformDispatcher.instance.engineId!);
37+
if (activity == null) return;
38+
activity.jClass
39+
.instanceMethodId("finish", "()V")
40+
.call(activity, jvoid.type, []);
41+
activity.release();
3742
}
3843

3944
void showToast(String text) {
40-
// This is example for calling your app's custom java code.
41-
// Place the Toaster class in the app's android/ source Folder, with a Keep
42-
// annotation or appropriate proguard rules to retain classes in release mode.
43-
//
44-
// In this example, Toaster class wraps android.widget.Toast so that it
45-
// can be called from any thread. See
46-
// android/app/src/main/java/com/github/dart_lang/jni_example/Toaster.java
45+
final activity = Jni.androidActivity(PlatformDispatcher.instance.engineId!);
46+
if (activity == null) return;
4747
final toasterClass =
4848
JClass.forName('com/github/dart_lang/jni_example/Toaster');
4949
final makeText = toasterClass.staticMethodId(
5050
'makeText',
5151
'(Landroid/app/Activity;Landroid/content/Context;'
5252
'Ljava/lang/CharSequence;I)'
5353
'Lcom/github/dart_lang/jni_example/Toaster;');
54-
final toaster = makeText.call(toasterClass, JObject.type, [
55-
Jni.getCurrentActivity(),
56-
Jni.getCachedApplicationContext(),
57-
'😀'.toJString(),
54+
final applicationContext =
55+
Jni.androidApplicationContext(PlatformDispatcher.instance.engineId!);
56+
final toaster = makeText(toasterClass, JObject.type, [
57+
activity,
58+
applicationContext,
59+
text.toJString(),
5860
0,
5961
]);
6062
final show = toasterClass.instanceMethodId('show', '()V');
6163
show(toaster, jvoid.type, []);
64+
toaster.release();
65+
applicationContext.release();
66+
activity.release();
67+
text.toJString().release();
6268
}
6369

6470
void main() {
71+
WidgetsFlutterBinding.ensureInitialized();
6572
if (!Platform.isAndroid) {
6673
Jni.spawn();
6774
}
@@ -80,13 +87,22 @@ void main() {
8087
}),
8188
Example(
8289
"Package name",
83-
() => JObject.fromReference(Jni.getCurrentActivity()).use((activity) =>
84-
activity.jClass
85-
.instanceMethodId("getPackageName", "()Ljava/lang/String;")
86-
.call(activity, JString.type, [])),
90+
() {
91+
final activity =
92+
Jni.androidActivity(PlatformDispatcher.instance.engineId!);
93+
if (activity == null) return "Activity not available";
94+
final packageName = activity.jClass
95+
.instanceMethodId("getPackageName", "()Ljava/lang/String;")
96+
.call(activity, JString.type, []);
97+
activity.release();
98+
return packageName;
99+
},
100+
),
101+
Example(
102+
"Show toast",
103+
() => showToast("Hello from JNI!"),
104+
runInitially: false,
87105
),
88-
Example("Show toast", () => showToast("Hello from JNI!"),
89-
runInitially: false),
90106
Example(
91107
"Quit",
92108
quit,
@@ -104,20 +120,10 @@ class Example {
104120
Example(this.title, this.callback, {this.runInitially = true});
105121
}
106122

107-
class MyApp extends StatefulWidget {
123+
class MyApp extends StatelessWidget {
108124
const MyApp(this.examples, {super.key});
109125
final List<Example> examples;
110126

111-
@override
112-
_MyAppState createState() => _MyAppState();
113-
}
114-
115-
class _MyAppState extends State<MyApp> {
116-
@override
117-
void initState() {
118-
super.initState();
119-
}
120-
121127
@override
122128
Widget build(BuildContext context) {
123129
return MaterialApp(
@@ -126,11 +132,12 @@ class _MyAppState extends State<MyApp> {
126132
title: const Text('JNI Examples'),
127133
),
128134
body: ListView.builder(
129-
itemCount: widget.examples.length,
130-
itemBuilder: (context, i) {
131-
final eg = widget.examples[i];
132-
return ExampleCard(eg);
133-
}),
135+
itemCount: examples.length,
136+
itemBuilder: (context, i) {
137+
final eg = examples[i];
138+
return ExampleCard(eg);
139+
},
140+
),
134141
),
135142
);
136143
}

0 commit comments

Comments
 (0)