Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions .github/workflows/jnigen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 .
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions pkgs/jni/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long, JniPlugin> pluginMap = new ConcurrentHashMap<>();

private long engineId;
private Context context;
private Activity activity;
private final List<ActivityListener> 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());
}
}
72 changes: 45 additions & 27 deletions pkgs/jni/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import 'dart:ffi';
import 'dart:io';
import 'dart:ui';

import 'package:ffi/ffi.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -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.
Expand All @@ -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,
]);
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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});
}
Expand All @@ -158,8 +163,12 @@ class MyApp extends StatefulWidget {
}

class _MyAppState extends State<MyApp> {
late Stream<JObject?> activityStream;

@override
void initState() {
activityStream =
Jni.androidActivities(PlatformDispatcher.instance.engineId!);
super.initState();
}

Expand All @@ -170,20 +179,29 @@ class _MyAppState extends State<MyApp> {
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!);
},
);
}),
),
);
}
}

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();
Expand All @@ -210,7 +228,7 @@ class _ExampleCardState extends State<ExampleCard> {
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();
Expand Down
Loading
Loading