diff --git a/.gitignore b/.gitignore
index 714b5d9..57b84ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ xcuserdata/
*.moved-aside
*.xccheckout
*.xcscmblueprint
+*.DS_Store
## Obj-C/Swift specific
*.hmap
@@ -70,4 +71,9 @@ AndroidBluetooth.xcodeproj/*
Package.resolved
# VS Code
-.vscode
\ No newline at end of file
+.vscode
+
+# Android
+*.so
+.gradle
+.idea
\ No newline at end of file
diff --git a/Demo/Package.swift b/Demo/Package.swift
new file mode 100644
index 0000000..ab35f25
--- /dev/null
+++ b/Demo/Package.swift
@@ -0,0 +1,44 @@
+// swift-tools-version: 6.2
+import PackageDescription
+
+let package = Package(
+ name: "SwiftAndroidApp",
+ platforms: [
+ .macOS(.v15),
+ ],
+ products: [
+ .library(
+ name: "SwiftAndroidApp",
+ type: .dynamic,
+ targets: ["SwiftAndroidApp"]
+ ),
+ ],
+ dependencies: [
+ .package(
+ path: "../"
+ ),
+ .package(
+ url: "https://github.com/PureSwift/Android.git",
+ branch: "master"
+ ),
+ ],
+ targets: [
+ .target(
+ name: "SwiftAndroidApp",
+ dependencies: [
+ .product(
+ name: "AndroidBluetooth",
+ package: "AndroidBluetooth"
+ ),
+ .product(
+ name: "AndroidKit",
+ package: "Android"
+ )
+ ],
+ path: "./app/src/main/swift",
+ swiftSettings: [
+ .swiftLanguageMode(.v5)
+ ]
+ )
+ ]
+)
diff --git a/Demo/app/.gitignore b/Demo/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/Demo/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Demo/app/build.gradle.kts b/Demo/app/build.gradle.kts
new file mode 100644
index 0000000..4bed26e
--- /dev/null
+++ b/Demo/app/build.gradle.kts
@@ -0,0 +1,89 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+}
+
+android {
+ namespace = "com.pureswift.swiftandroid"
+ compileSdk = 35
+
+ defaultConfig {
+ applicationId = "com.pureswift.swiftandroid"
+ minSdk = 29
+ targetSdk = 35
+ versionCode = 1
+ versionName = "1.0"
+ ndk {
+ //noinspection ChromeOsAbiSupport
+ abiFilters += listOf("arm64-v8a")
+ }
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ compose = true
+ }
+ packaging {
+ resources {
+ excludes += listOf("/META-INF/{AL2.0,LGPL2.1}")
+ }
+ jniLibs {
+ keepDebugSymbols += listOf(
+ "*/arm64-v8a/*.so",
+ "*/armeabi-v7a/*.so",
+ "*/x86_64/*.so"
+ )
+ }
+ }
+}
+
+// Compile native Swift code for the demo app with `skip android build`.
+val buildSwift by tasks.registering(Exec::class) {
+ group = "build"
+ description = "Build native Swift sources for Android"
+ workingDir(rootProject.projectDir)
+ commandLine("bash", "build-swift.sh")
+}
+
+tasks.named("preBuild") {
+ dependsOn(buildSwift)
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.ui)
+ implementation(libs.androidx.ui.graphics)
+ implementation(libs.androidx.ui.tooling.preview)
+ implementation(libs.androidx.recyclerview)
+ implementation(libs.androidx.navigation.runtime)
+ implementation(libs.androidx.material3)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ androidTestImplementation(platform(libs.androidx.compose.bom))
+ androidTestImplementation(libs.androidx.ui.test.junit4)
+ debugImplementation(libs.androidx.ui.tooling)
+ debugImplementation(libs.androidx.ui.test.manifest)
+}
diff --git a/Demo/app/proguard-rules.pro b/Demo/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Demo/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/Demo/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt b/Demo/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..0b3aecb
--- /dev/null
+++ b/Demo/app/src/androidTest/java/com/pureswift/swiftandroid/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.pureswift.swiftandroid
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.pureswift.swiftandroid", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/AndroidManifest.xml b/Demo/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d9a561f
--- /dev/null
+++ b/Demo/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/java/com/example/swift/HelloSubclass.java b/Demo/app/src/main/java/com/example/swift/HelloSubclass.java
new file mode 100644
index 0000000..2312d8f
--- /dev/null
+++ b/Demo/app/src/main/java/com/example/swift/HelloSubclass.java
@@ -0,0 +1,27 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+public class HelloSubclass extends HelloSwift {
+ private String greeting;
+
+ public HelloSubclass(String greeting) {
+ this.greeting = greeting;
+ }
+
+ public void greetMe() {
+ super.greet(greeting);
+ }
+}
diff --git a/Demo/app/src/main/java/com/example/swift/HelloSwift.java b/Demo/app/src/main/java/com/example/swift/HelloSwift.java
new file mode 100644
index 0000000..5a1ba2e
--- /dev/null
+++ b/Demo/app/src/main/java/com/example/swift/HelloSwift.java
@@ -0,0 +1,64 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.util.function.Predicate;
+
+public class HelloSwift {
+ public double value;
+ public static double initialValue = 3.14159;
+ public String name = "Java";
+
+ static {
+ System.loadLibrary("SwiftAndroidApp");
+ }
+
+ public HelloSwift() {
+ this.value = initialValue;
+ }
+
+ public native int sayHello(int x, int y);
+ public native String throwMessageFromSwift(String message) throws Exception;
+
+ // To be called back by the native code
+ public double sayHelloBack(int i) {
+ System.out.println("And hello back from " + name + "! You passed me " + i);
+ return value;
+ }
+
+ public void greet(String name) {
+ System.out.println("Salutations, " + name);
+ }
+
+ public Predicate lessThanTen() {
+ Predicate predicate = i -> (i < 10);
+ return predicate;
+ }
+
+ public String[] doublesToStrings(double[] doubles) {
+ int size = doubles.length;
+ String[] strings = new String[size];
+
+ for(int i = 0; i < size; i++) {
+ strings[i] = "" + doubles[i];
+ }
+
+ return strings;
+ }
+
+ public void throwMessage(String message) throws Exception {
+ throw new Exception(message);
+ }
+}
diff --git a/Demo/app/src/main/java/com/example/swift/ThreadSafe.java b/Demo/app/src/main/java/com/example/swift/ThreadSafe.java
new file mode 100644
index 0000000..2b1b358
--- /dev/null
+++ b/Demo/app/src/main/java/com/example/swift/ThreadSafe.java
@@ -0,0 +1,22 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ThreadSafe {
+}
diff --git a/Demo/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java b/Demo/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java
new file mode 100644
index 0000000..3b7793f
--- /dev/null
+++ b/Demo/app/src/main/java/com/example/swift/ThreadSafeHelperClass.java
@@ -0,0 +1,41 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+package com.example.swift;
+
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.OptionalInt;
+import java.util.OptionalDouble;
+
+@ThreadSafe
+public class ThreadSafeHelperClass {
+ public ThreadSafeHelperClass() { }
+
+ public Optional text = Optional.of("");
+
+ public final OptionalDouble val = OptionalDouble.of(2);
+
+ public String getValue(Optional name) {
+ return name.orElse("");
+ }
+
+ public Optional getText() {
+ return text;
+ }
+
+ public OptionalLong from(OptionalInt value) {
+ return OptionalLong.of(value.getAsInt());
+ }
+}
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Application.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Application.kt
new file mode 100644
index 0000000..4075c97
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Application.kt
@@ -0,0 +1,29 @@
+package com.pureswift.swiftandroid
+
+import com.example.swift.HelloSubclass
+
+class Application: android.app.Application() {
+
+ init {
+ NativeLibrary.shared()
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ onCreateSwift()
+ }
+
+ private external fun onCreateSwift()
+
+ override fun onTerminate() {
+ super.onTerminate()
+ onTerminateSwift()
+ }
+
+ private external fun onTerminateSwift()
+
+ fun sayHello() {
+ val result = HelloSubclass("Swift").sayHello(17, 25)
+ println("sayHello(17, 25) = $result")
+ }
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt
new file mode 100644
index 0000000..5b20617
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/EmbeddedAndroidViewDemo.kt
@@ -0,0 +1,63 @@
+package com.pureswift.swiftandroid
+
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.material3.Text
+import androidx.compose.material3.Button
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+
+@Preview(showBackground = true)
+@Composable
+fun EmbeddedAndroidViewDemo() {
+ Column {
+ val state = remember { mutableIntStateOf(0) }
+
+ //widget.ImageView
+ AndroidView(factory = { ctx ->
+ ImageView(ctx).apply {
+ val drawable = ContextCompat.getDrawable(ctx, R.drawable.ic_launcher_foreground)
+ setImageDrawable(drawable)
+ }
+ })
+
+ //Compose Button
+ Button(onClick = { state.value++ }) {
+ Text("MyComposeButton")
+ }
+
+ //widget.Button
+ AndroidView(factory = { ctx ->
+ //Here you can construct your View
+ android.widget.Button(ctx).apply {
+ text = "MyAndroidButton"
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ setOnClickListener {
+ state.value++
+ }
+ }
+ }, modifier = Modifier.padding(8.dp))
+
+ //widget.TextView
+ AndroidView(factory = { ctx ->
+ //Here you can construct your View
+ TextView(ctx).apply {
+ layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+ }
+ }, update = {
+ it.text = "You have clicked the buttons: " + state.value.toString() + " times"
+ })
+ }
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Fragment.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Fragment.kt
new file mode 100644
index 0000000..26a6c39
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Fragment.kt
@@ -0,0 +1,24 @@
+package com.pureswift.swiftandroid
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+
+class Fragment(val swiftObject: SwiftObject): android.app.Fragment() {
+
+ @Deprecated("Deprecated in Java")
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ val context = this.context
+ checkNotNull(context)
+ val linearLayout = LinearLayout(context)
+ return linearLayout
+ }
+
+ external override fun onViewCreated(view: View, savedInstanceState: Bundle?)
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ListViewAdapter.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ListViewAdapter.kt
new file mode 100644
index 0000000..722b2b0
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ListViewAdapter.kt
@@ -0,0 +1,14 @@
+package com.pureswift.swiftandroid
+
+import android.R
+import android.content.Context
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.recyclerview.widget.RecyclerView
+
+class ListViewAdapter(context: Context, val swiftObject: SwiftObject, val objects: ArrayList) :
+ ArrayAdapter(context, 0, objects) {
+
+ external override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
+}
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/MainActivity.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/MainActivity.kt
new file mode 100644
index 0000000..2f3c2eb
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/MainActivity.kt
@@ -0,0 +1,83 @@
+package com.pureswift.swiftandroid
+
+import android.Manifest
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.core.app.ActivityCompat
+import java.util.Date
+
+class MainActivity : ComponentActivity() {
+
+ init {
+ NativeLibrary.shared()
+ }
+
+ val emitter = UnitEmitter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ onCreateSwift(savedInstanceState)
+ //enableEdgeToEdge()
+
+ // Request permissions on startup.
+ val permissions = listOf(
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.INTERNET
+ )
+ val requestTag = 1
+ ActivityCompat.requestPermissions(this, permissions.toTypedArray(), requestTag)
+ }
+
+ external fun onCreateSwift(savedInstanceState: Bundle?)
+
+ fun setRootView(view: View) {
+ Log.d("MainActivity", "AndroidSwiftUI.MainActivity.setRootView(_:)")
+ setContentView(view)
+ }
+}
+
+@Composable
+fun EventReceiver(emitter: UnitEmitter) {
+
+ val tick by emitter.flow.collectAsState(initial = Unit)
+
+ var date by remember { mutableStateOf(Date()) }
+
+ LaunchedEffect(Unit) {
+ emitter.flow.collect {
+ date = Date()
+ }
+ }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text("Hello Swift!")
+ Text(date.toString())
+ }
+ }
+}
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NativeLibrary.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NativeLibrary.kt
new file mode 100644
index 0000000..871a45c
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NativeLibrary.kt
@@ -0,0 +1,35 @@
+package com.pureswift.swiftandroid
+
+import android.util.Log
+
+class NativeLibrary private constructor() {
+
+ companion object {
+
+ @Volatile
+ var shared : NativeLibrary? = null
+
+ fun shared(): NativeLibrary {
+ return shared?: synchronized(this){
+ val instance = NativeLibrary()
+ shared = instance
+ return instance
+ }
+ }
+ }
+
+ init {
+ loadNativeLibrary()
+ }
+
+ private fun loadNativeLibrary() {
+ try {
+ System.loadLibrary("SwiftAndroidApp")
+ System.loadLibrary("SwiftJava")
+ } catch (error: UnsatisfiedLinkError) {
+ Log.e("NativeLibrary", "Unable to load native libraries: $error")
+ return
+ }
+ Log.d("NativeLibrary", "Loaded Swift library")
+ }
+}
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt
new file mode 100644
index 0000000..cf8014f
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/NavigationBarViewOnItemSelectedListener.kt
@@ -0,0 +1,9 @@
+package com.pureswift.swiftandroid
+
+import android.view.MenuItem
+import com.google.android.material.navigation.NavigationBarView
+
+class NavigationBarViewOnItemSelectedListener(val action: SwiftObject): NavigationBarView.OnItemSelectedListener {
+
+ external override fun onNavigationItemSelected(menuItem: MenuItem): Boolean
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/RecyclerViewAdapter.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/RecyclerViewAdapter.kt
new file mode 100644
index 0000000..d11648d
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/RecyclerViewAdapter.kt
@@ -0,0 +1,40 @@
+package com.pureswift.swiftandroid
+
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+class RecyclerViewAdapter(val swiftObject: SwiftObject) :
+ RecyclerView.Adapter() {
+
+ class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+
+ }
+
+ // Create new views (invoked by the layout manager)
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewAdapter.ViewHolder {
+ Log.d("RecyclerViewAdapter", "SwiftAndroidApp.RecyclerViewAdapter.onCreateViewHolderSwift(_:_:) $viewType")
+ val view = LinearLayout(parent.context)
+ val viewHolder = ViewHolder(view)
+ checkNotNull(viewHolder)
+ checkNotNull(viewHolder.itemView)
+ return viewHolder
+ }
+
+ // Replace the contents of a view (invoked by the layout manager)
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ onBindViewHolderSwift(holder as RecyclerViewAdapter.ViewHolder, position)
+ }
+
+ external fun onBindViewHolderSwift(holder: RecyclerViewAdapter.ViewHolder, position: Int)
+
+ // Return the size of your dataset (invoked by the layout manager)
+ override fun getItemCount(): Int {
+ return getItemCountSwift()
+ }
+
+ external fun getItemCountSwift(): Int
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Runnable.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Runnable.kt
new file mode 100644
index 0000000..08a0987
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/Runnable.kt
@@ -0,0 +1,6 @@
+package com.pureswift.swiftandroid
+
+class Runnable(val block: SwiftObject): java.lang.Runnable {
+
+ external override fun run()
+}
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/SwiftObject.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/SwiftObject.kt
new file mode 100644
index 0000000..94ff2db
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/SwiftObject.kt
@@ -0,0 +1,17 @@
+package com.pureswift.swiftandroid
+
+/// Swift object retained by JVM
+class SwiftObject(val swiftObject: Long, val type: String) {
+
+ override fun toString(): String {
+ return toStringSwift()
+ }
+
+ external fun toStringSwift(): String
+
+ fun finalize() {
+ finalizeSwift()
+ }
+
+ external fun finalizeSwift()
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/UnitEmitter.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/UnitEmitter.kt
new file mode 100644
index 0000000..93af004
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/UnitEmitter.kt
@@ -0,0 +1,15 @@
+package com.pureswift.swiftandroid
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+
+class UnitEmitter() {
+
+ private val _flow = MutableSharedFlow(extraBufferCapacity = 64)
+ val flow: SharedFlow get() = _flow
+
+ fun emit() {
+ //println("Emit")
+ _flow.tryEmit(Unit)
+ }
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ViewOnClickListener.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ViewOnClickListener.kt
new file mode 100644
index 0000000..39b5ce3
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ViewOnClickListener.kt
@@ -0,0 +1,8 @@
+package com.pureswift.swiftandroid
+
+import android.view.View
+
+class ViewOnClickListener(val action: SwiftObject): View.OnClickListener {
+
+ external override fun onClick(view: View)
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Color.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Color.kt
new file mode 100644
index 0000000..4a8371b
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Color.kt
@@ -0,0 +1,11 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Theme.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Theme.kt
new file mode 100644
index 0000000..47f8f81
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Theme.kt
@@ -0,0 +1,58 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import android.app.Activity
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+ primary = Purple80,
+ secondary = PurpleGrey80,
+ tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+ primary = Purple40,
+ secondary = PurpleGrey40,
+ tertiary = Pink40
+
+ /* Other default colors to override
+ background = Color(0xFFFFFBFE),
+ surface = Color(0xFFFFFBFE),
+ onPrimary = Color.White,
+ onSecondary = Color.White,
+ onTertiary = Color.White,
+ onBackground = Color(0xFF1C1B1F),
+ onSurface = Color(0xFF1C1B1F),
+ */
+)
+
+@Composable
+fun SwiftAndroidTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ // Dynamic color is available on Android 12+
+ dynamicColor: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val colorScheme = when {
+ dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ val context = LocalContext.current
+ if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+ }
+
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Type.kt b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Type.kt
new file mode 100644
index 0000000..4de94b7
--- /dev/null
+++ b/Demo/app/src/main/kotlin/com/pureswift/swiftandroid/ui/theme/Type.kt
@@ -0,0 +1,34 @@
+package com.pureswift.swiftandroid.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ bodyLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.5.sp
+ )
+ /* Other default text styles to override
+ titleLarge = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Medium,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/Demo/app/src/main/kotlin/org/pureswift/bluetooth/le/ScanCallback.kt b/Demo/app/src/main/kotlin/org/pureswift/bluetooth/le/ScanCallback.kt
new file mode 100644
index 0000000..f4d701a
--- /dev/null
+++ b/Demo/app/src/main/kotlin/org/pureswift/bluetooth/le/ScanCallback.kt
@@ -0,0 +1,87 @@
+package org.pureswift.bluetooth.le
+
+import android.bluetooth.le.ScanCallback as AndroidScanCallback
+import android.bluetooth.le.ScanResult
+import android.util.Log
+
+/**
+ * Bluetooth LE Scan Callback for AndroidBluetooth Swift package.
+ *
+ * This class is referenced by AndroidBluetooth's LowEnergyScanCallback
+ * via the @JavaClass("org.pureswift.bluetooth.le.ScanCallback") annotation.
+ * It extends Android's ScanCallback and provides the bridge between
+ * Android's Bluetooth LE scanning and the Swift AndroidBluetooth package.
+ */
+open class ScanCallback(
+ private var swiftPeer: Long = 0L
+) : AndroidScanCallback() {
+
+ fun setSwiftPeer(swiftPeer: Long) {
+ this.swiftPeer = swiftPeer
+ }
+
+ fun getSwiftPeer(): Long {
+ return swiftPeer
+ }
+
+ fun finalize() {
+ swiftRelease(swiftPeer)
+ swiftPeer = 0L
+ }
+
+ private external fun swiftRelease(swiftPeer: Long)
+
+ companion object {
+ private const val TAG = "PureSwift.ScanCallback"
+ }
+
+ /**
+ * Callback when a BLE advertisement has been found.
+ *
+ * @param callbackType Determines how this callback was triggered
+ * @param result A Bluetooth LE scan result
+ */
+ override fun onScanResult(callbackType: Int, result: ScanResult?) {
+ super.onScanResult(callbackType, result)
+ swiftOnScanResult(swiftPeer, callbackType, result)
+ }
+
+ external fun swiftOnScanResult(
+ swiftPeer: Long,
+ callbackType: Int,
+ result: ScanResult?
+ )
+
+ /**
+ * Callback when batch results are delivered.
+ *
+ * @param results List of scan results that are previously scanned
+ */
+ override fun onBatchScanResults(results: MutableList?) {
+ super.onBatchScanResults(results)
+ if (swiftPeer != 0L) {
+ swiftOnBatchScanResults(swiftPeer, results)
+ } else {
+ Log.d(TAG, "onBatchScanResults: ${results?.size ?: 0} results")
+ }
+ }
+ private external fun swiftOnBatchScanResults(
+ swiftPeer: Long,
+ results: MutableList?
+ )
+
+ /**
+ * Callback when scan could not be started.
+ *
+ * @param errorCode Error code (one of SCAN_FAILED_*)
+ */
+ override fun onScanFailed(errorCode: Int) {
+ super.onScanFailed(errorCode)
+ if (swiftPeer != 0L) {
+ swiftOnScanFailed(swiftPeer, errorCode)
+ } else {
+ Log.e(TAG, "onScanFailed: errorCode=$errorCode")
+ }
+ }
+ private external fun swiftOnScanFailed(swiftPeer: Long, errorCode: Int)
+}
diff --git a/Demo/app/src/main/res/drawable/ic_launcher_background.xml b/Demo/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/Demo/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Demo/app/src/main/res/drawable/ic_launcher_foreground.xml b/Demo/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/Demo/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/layout/list_item.xml b/Demo/app/src/main/res/layout/list_item.xml
new file mode 100644
index 0000000..eaf5504
--- /dev/null
+++ b/Demo/app/src/main/res/layout/list_item.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/Demo/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Demo/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/Demo/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Demo/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/Demo/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Demo/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/Demo/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Demo/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/Demo/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/Demo/app/src/main/res/values/colors.xml b/Demo/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/Demo/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/values/strings.xml b/Demo/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..7de10d2
--- /dev/null
+++ b/Demo/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ SwiftAndroid
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/values/themes.xml b/Demo/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..c81f2c7
--- /dev/null
+++ b/Demo/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/xml/backup_rules.xml b/Demo/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/Demo/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/res/xml/data_extraction_rules.xml b/Demo/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/Demo/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Demo/app/src/main/swift/Application.swift b/Demo/app/src/main/swift/Application.swift
new file mode 100644
index 0000000..b2b4d78
--- /dev/null
+++ b/Demo/app/src/main/swift/Application.swift
@@ -0,0 +1,74 @@
+//
+// SwiftApp.swift
+// SwiftAndroidApp
+//
+// Created by Alsey Coleman Miller on 6/8/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.Application")
+public class Application: AndroidApp.Application {
+
+ @JavaMethod
+ public func sayHello()
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Application")
+public extension Application {
+
+ @JavaMethod
+ func onCreateSwift() {
+ log("\(self).\(#function)")
+
+ printAPIVersion()
+ sayHello()
+ }
+
+ @JavaMethod
+ func onTerminateSwift() {
+ log("\(self).\(#function)")
+ }
+}
+
+private extension Application {
+
+ func printAPIVersion() {
+
+ do {
+ let api = try AndroidOS.AndroidAPI.current
+ Self.logInfo("Running on Android API \(api)")
+ }
+ catch {
+ Self.logError("\(error)")
+ }
+ }
+}
+
+extension Application {
+
+ static var logTag: LogTag { "Application" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/Fragment.swift b/Demo/app/src/main/swift/Fragment.swift
new file mode 100644
index 0000000..a058472
--- /dev/null
+++ b/Demo/app/src/main/swift/Fragment.swift
@@ -0,0 +1,86 @@
+//
+// Fragment.swift
+// SwiftAndroidApp
+//
+// Created by Alsey Coleman Miller on 6/22/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.Fragment")
+open class Fragment: AndroidApp.Fragment {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getSwiftObject() -> SwiftObject?
+}
+
+public extension Fragment {
+
+ struct Callback {
+
+ var onViewCreated: ((AndroidView.View, AndroidOS.Bundle?) -> ())?
+ }
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Fragment")
+extension Fragment {
+
+ @JavaMethod
+ func onViewCreated(
+ view: AndroidView.View?,
+ savedInstanceState: AndroidOS.Bundle?
+ ) {
+ log("\(self).\(#function)")
+ guard let onViewCreated = callback.onViewCreated else {
+ return
+ }
+ guard let view else {
+ assertionFailure("Missing view")
+ return
+ }
+ onViewCreated(view, savedInstanceState)
+ }
+}
+
+public extension Fragment {
+
+ convenience init(callback: Callback, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(callback, environment: environment)
+ self.init(swiftObject: object, environment: environment)
+ }
+
+ var callback: Callback {
+ getSwiftObject()!.valueObject().value as! Callback
+ }
+}
+
+extension Fragment {
+
+ static var logTag: LogTag { "Fragment" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/HelloSubclass.swift b/Demo/app/src/main/swift/HelloSubclass.swift
new file mode 100644
index 0000000..8434d77
--- /dev/null
+++ b/Demo/app/src/main/swift/HelloSubclass.swift
@@ -0,0 +1,16 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import SwiftJava
+import CSwiftJavaJNI
+
+@JavaClass("com.example.swift.HelloSubclass")
+open class HelloSubclass: HelloSwift {
+ @JavaMethod
+ @_nonoverride public convenience init(_ greeting: String, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func greetMe()
+}
+extension JavaClass {
+ @JavaStaticField(isFinal: false)
+ public var initialValue: Double
+}
diff --git a/Demo/app/src/main/swift/HelloSwift.swift b/Demo/app/src/main/swift/HelloSwift.swift
new file mode 100644
index 0000000..abd7f54
--- /dev/null
+++ b/Demo/app/src/main/swift/HelloSwift.swift
@@ -0,0 +1,46 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import SwiftJava
+import JavaUtilFunction
+import CSwiftJavaJNI
+
+@JavaClass("com.example.swift.HelloSwift")
+open class HelloSwift: JavaObject {
+ @JavaField(isFinal: false)
+ public var value: Double
+
+ @JavaField(isFinal: false)
+ public var name: String
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func greet(_ name: String)
+
+ @JavaMethod
+ open func sayHelloBack(_ i: Int32) -> Double
+
+ @JavaMethod
+ open func lessThanTen() -> JavaPredicate!
+
+ @JavaMethod
+ open func doublesToStrings(_ doubles: [Double]) -> [String]
+
+ @JavaMethod
+ open func throwMessage(_ message: String) throws
+}
+extension JavaClass {
+ @JavaStaticField(isFinal: false)
+ public var initialValue: Double
+}
+/// Describes the Java `native` methods for ``HelloSwift``.
+///
+/// To implement all of the `native` methods for HelloSwift in Swift,
+/// extend HelloSwift to conform to this protocol and mark
+/// each implementation of the protocol requirement with
+/// `@JavaMethod`.
+protocol HelloSwiftNativeMethods {
+ func throwMessageFromSwift(_ message: String) throws -> String
+
+ func sayHello(_ x: Int32, _ y: Int32) -> Int32
+}
diff --git a/Demo/app/src/main/swift/JavaKitExample.swift b/Demo/app/src/main/swift/JavaKitExample.swift
new file mode 100644
index 0000000..1ca3b3d
--- /dev/null
+++ b/Demo/app/src/main/swift/JavaKitExample.swift
@@ -0,0 +1,115 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of Swift.org project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import SwiftJava
+import JavaUtilFunction
+import AndroidUtil
+import AndroidLogging
+
+enum SwiftWrappedError: Error {
+ case message(String)
+}
+
+@JavaImplementation("com.example.swift.HelloSwift")
+extension HelloSwift: HelloSwiftNativeMethods {
+ @JavaMethod
+ func sayHello(_ i: Int32, _ j: Int32) -> Int32 {
+ print("Hello from Swift!")
+ let answer = self.sayHelloBack(i + j)
+ print("Swift got back \(answer) from Java")
+
+ print("We expect the above value to be the initial value, \(self.javaClass.initialValue)")
+
+ print("Updating Java field value to something different")
+ self.value = 2.71828
+
+ let newAnswer = self.sayHelloBack(17)
+ print("Swift got back updated \(newAnswer) from Java")
+
+ let newHello = HelloSwift(environment: javaEnvironment)
+ print("Swift created a new Java instance with the value \(newHello.value)")
+
+ let name = newHello.name
+ print("Hello to \(name)")
+ newHello.greet("Swift 👋🏽 How's it going")
+
+ self.name = "a 🗑️-collected language"
+ _ = self.sayHelloBack(42)
+
+ let predicate: JavaPredicate = self.lessThanTen()!
+ let value = predicate.test(JavaInteger(3).as(JavaObject.self))
+ print("Running a JavaPredicate from swift 3 < 10 = \(value)")
+
+ let strings = doublesToStrings([3.14159, 2.71828])
+ print("Converting doubles to strings: \(strings)")
+
+ // Try downcasting
+ if let helloSub = self.as(HelloSubclass.self) {
+ print("Hello from the subclass!")
+ helloSub.greetMe()
+
+ assert(helloSub.value == 2.71828)
+ } else {
+ fatalError("Expected subclass here")
+ }
+
+ // Check "is" behavior
+ assert(newHello.is(HelloSwift.self))
+ assert(!newHello.is(HelloSubclass.self))
+
+ // Create a new instance.
+ let helloSubFromSwift = HelloSubclass("Hello from Swift", environment: javaEnvironment)
+ helloSubFromSwift.greetMe()
+
+ do {
+ try throwMessage("I am an error")
+ } catch {
+ print("Caught Java error: \(error)")
+ }
+
+ // Make sure that the thread safe class is sendable
+ let helper = ThreadSafeHelperClass(environment: javaEnvironment)
+ let threadSafe: Sendable = helper
+
+ checkOptionals(helper: helper)
+
+ return i * j
+ }
+
+ func checkOptionals(helper: ThreadSafeHelperClass) {
+ let text: JavaString? = helper.textOptional
+ let value: String? = helper.getValueOptional(Optional.none)
+ let textFunc: JavaString? = helper.getTextOptional()
+ let doubleOpt: Double? = helper.valOptional
+ let longOpt: Int64? = helper.fromOptional(21 as Int32?)
+ print("Optional text = \(text.debugDescription)")
+ print("Optional string value = \(value.debugDescription)")
+ print("Optional text function returned \(textFunc.debugDescription)")
+ print("Optional double function returned \(doubleOpt.debugDescription)")
+ print("Optional long function returned \(longOpt.debugDescription)")
+ }
+
+ @JavaMethod
+ func throwMessageFromSwift(_ message: String) throws -> String {
+ throw SwiftWrappedError.message(message)
+ }
+}
+
+internal extension HelloSwift {
+
+ func print(_ string: String) {
+ try? AndroidLogger(tag: "HelloSwift", priority: .verbose)
+ .log(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/JavaRetainedValue.swift b/Demo/app/src/main/swift/JavaRetainedValue.swift
new file mode 100644
index 0000000..4228e6e
--- /dev/null
+++ b/Demo/app/src/main/swift/JavaRetainedValue.swift
@@ -0,0 +1,89 @@
+//
+// JavaRetainedValue.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import SwiftJava
+import CSwiftJavaJNI
+
+/// Java class that retains a Swift value for the duration of its lifetime.
+@JavaClass("com.pureswift.swiftandroid.SwiftObject")
+open class SwiftObject: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: Int64, type: String, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func getSwiftObject() -> Int64
+
+ @JavaMethod
+ open func getType() -> String
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.SwiftObject")
+extension SwiftObject {
+
+ @JavaMethod
+ public func toStringSwift() -> String {
+ "\(valueObject().value)"
+ }
+
+ @JavaMethod
+ public func finalizeSwift() {
+ // release owned swift value
+ release()
+ }
+}
+
+extension SwiftObject {
+
+ convenience init(_ value: T, environment: JNIEnvironment? = nil) {
+ let box = JavaRetainedValue(value)
+ let type = box.type
+ self.init(swiftObject: box.id, type: type, environment: environment)
+ // retain value
+ retain(box)
+ }
+
+ func valueObject() -> JavaRetainedValue {
+ let id = getSwiftObject()
+ guard let object = Self.retained[id] else {
+ fatalError()
+ }
+ return object
+ }
+}
+
+private extension SwiftObject {
+
+ static var retained = [JavaRetainedValue.ID: JavaRetainedValue]()
+
+ func retain(_ value: JavaRetainedValue) {
+ Self.retained[value.id] = value
+ }
+
+ func release() {
+ let id = getSwiftObject()
+ Self.retained[id] = nil
+ }
+}
+
+/// Swift Object retained until released by Java object.
+final class JavaRetainedValue: Identifiable {
+
+ var value: Any
+
+ var type: String {
+ String(describing: Swift.type(of: value))
+ }
+
+ var id: Int64 {
+ Int64(ObjectIdentifier(self).hashValue)
+ }
+
+ init(_ value: T) {
+ self.value = value
+ }
+}
diff --git a/Demo/app/src/main/swift/ListViewAdapter.swift b/Demo/app/src/main/swift/ListViewAdapter.swift
new file mode 100644
index 0000000..bce0934
--- /dev/null
+++ b/Demo/app/src/main/swift/ListViewAdapter.swift
@@ -0,0 +1,87 @@
+//
+// ListViewAdapter.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import Foundation
+import SwiftJava
+import JavaUtil
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.ListViewAdapter", extends: ListAdapter.self)
+open class ListViewAdapter: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(
+ context: AndroidContent.Context?,
+ swiftObject: SwiftObject?,
+ objects: ArrayList?,
+ environment: JNIEnvironment? = nil
+ )
+
+ @JavaMethod
+ func getSwiftObject() -> SwiftObject!
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.ListViewAdapter")
+extension ListViewAdapter {
+
+ @JavaMethod
+ func getView(position: Int32, convertView: AndroidView.View?, parent: ViewGroup?) -> AndroidView.View? {
+ log("\(self).\(#function) \(position)")
+ return getView(position, convertView, parent)
+ }
+}
+
+public extension ListViewAdapter {
+
+ typealias GetView = (Int32, AndroidView.View?, ViewGroup?) -> AndroidView.View?
+
+ var getView: GetView {
+ get {
+ getSwiftObject().valueObject().value as! GetView
+ }
+ set {
+ getSwiftObject().valueObject().value = newValue
+ }
+ }
+
+ convenience init(
+ context: AndroidContent.Context,
+ getView: @escaping (Int32, AndroidView.View?, ViewGroup?) -> AndroidView.View?,
+ objects: ArrayList,
+ environment: JNIEnvironment? = nil
+ ) {
+ self.init(context: context, swiftObject: SwiftObject(getView), objects: objects, environment: environment)
+ }
+}
+
+extension ListViewAdapter {
+
+ static var logTag: LogTag { "ListViewAdapter" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/MainActivity.swift b/Demo/app/src/main/swift/MainActivity.swift
new file mode 100644
index 0000000..1a06c8c
--- /dev/null
+++ b/Demo/app/src/main/swift/MainActivity.swift
@@ -0,0 +1,409 @@
+//
+// Activity.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/8/25.
+//
+
+import Foundation
+import AndroidKit
+import AndroidBluetooth
+import Bluetooth
+import GATT
+import JavaLang
+#if canImport(Binder)
+import Binder
+#endif
+
+@JavaClass("com.pureswift.swiftandroid.MainActivity")
+open class MainActivity: AndroidApp.Activity {
+
+ @JavaMethod
+ open func setRootView(_ view: AndroidView.View?)
+
+ @JavaMethod
+ open func getEmitter() -> UnitEmitter!
+
+ static private(set) var shared: MainActivity!
+
+ lazy var textView = TextView(self)
+
+ lazy var listView = ListView(self)
+
+ lazy var recyclerView = RecyclerView(self)
+
+ lazy var button = AndroidWidget.Button(self)
+
+ lazy var emitter = getEmitter()!
+
+ lazy var rootViewID: Int32 = try! JavaClass().generateViewId()
+
+ var central: AndroidCentral?
+
+ var results = [AndroidCentral.Peripheral.ID: GATT.ScanData]()
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.MainActivity")
+extension MainActivity {
+
+ @JavaMethod
+ public func onCreateSwift(_ savedInstanceState: BaseBundle?) {
+ log("\(self).\(#function)")
+
+ _onCreate(savedInstanceState)
+ }
+}
+
+private extension MainActivity {
+
+ #if os(Android)
+ typealias MainActor = AndroidMainActor
+ #endif
+
+ func _onCreate(_ savedInstanceState: BaseBundle?) {
+
+ // setup singletons
+ if savedInstanceState == nil, MainActivity.shared == nil {
+ MainActivity.shared = self
+ startMainRunLoop()
+ runAsync()
+ }
+
+ // need to create views
+ setRootView()
+
+ // start scanning
+ let hostController = try! JavaClass().getDefaultAdapter()!
+ let context = self.as(AndroidContent.Context.self)!
+ let central = AndroidCentral(
+ hostController: hostController,
+ context: context
+ )
+ self.central = central
+
+ Task {
+ do {
+ log("Start Scan")
+ let scanStream = try await central.scan()
+ for try await result in scanStream {
+ log("Found: \(result)")
+ results[result.id] = result
+ }
+ }
+ catch {
+ log("Error: \(error.localizedDescription)")
+ }
+ }
+
+ #if canImport(Binder)
+ Task {
+ printBinderVersion()
+ }
+ #endif
+ }
+
+ func runAsync() {
+ RunLoop.main.run(until: Date() + 0.1)
+ }
+
+ func startMainRunLoop() {
+ #if os(Android)
+ guard AndroidMainActor.setupMainLooper() else {
+ fatalError("Unable to setup main loop")
+ }
+ #endif
+ }
+
+ func updateListView() {
+ let results = results.keys.sorted().map { $0.description }
+ setListView(results)
+ }
+
+ func setRootView() {
+ setTextView()
+ }
+
+ func setTextView() {
+ let linearLayout = LinearLayout(self)
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+ linearLayout.addView(textView)
+ setRootView(linearLayout)
+ // update view on timer
+ Task { [weak self] in
+ while let self {
+ await self.updateTextView()
+ try? await Task.sleep(for: .seconds(1))
+ }
+ }
+ }
+
+ func startEmitterTimer() {
+ // update view on timer
+ Task { [weak self] in
+ while let self {
+ await emit()
+ try? await Task.sleep(for: .seconds(1))
+ }
+ }
+ }
+
+ @MainActor
+ func emit() {
+ Self.log("\(self).\(#function)")
+ emitter.emit()
+ }
+
+ func setupNavigationStack() {
+
+ let fragmentContainer = FrameLayout(self)
+ fragmentContainer.setId(rootViewID)
+ let matchParent = try! JavaClass().MATCH_PARENT
+ fragmentContainer.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+
+ let homeFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+
+ let linearLayout = LinearLayout(self)
+ linearLayout.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+
+ let label = TextView(context)
+ label.text = "Home View"
+ label.gravity = .center
+ linearLayout.addView(label)
+
+ let button = Button(context)
+ button.text = "Push"
+ label.gravity = .center
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ linearLayout.addView(button)
+
+ view.as(ViewGroup.self)!.addView(linearLayout)
+ }))
+
+ // setup initial fragment
+ _ = getFragmentManager()
+ .beginTransaction()
+ .replace(rootViewID, homeFragment)
+ .commit()
+
+ // Set as the content view
+ setRootView(fragmentContainer)
+ }
+
+ func configureButton() {
+ button.text = "Push"
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ }
+
+ func didPushButton() {
+
+ let counter = getFragmentManager().getBackStackEntryCount() + 1
+
+ let detailFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+
+ let matchParent = try! JavaClass().MATCH_PARENT
+
+ let linearLayout = LinearLayout(self)
+ linearLayout.setLayoutParams(ViewGroup.LayoutParams(matchParent, matchParent))
+ linearLayout.orientation = .vertical
+ linearLayout.gravity = .center
+
+ let label = TextView(context)
+ label.text = "Detail View \(counter)"
+ label.gravity = .center
+ linearLayout.addView(label)
+
+ let button = Button(context)
+ button.text = "Push"
+ button.gravity = .center
+ let listener = ViewOnClickListener {
+ self.didPushButton()
+ }
+ button.setOnClickListener(listener.as(View.OnClickListener.self))
+ linearLayout.addView(button)
+
+ view.as(ViewGroup.self)!.addView(linearLayout)
+ }))
+
+ push(detailFragment, name: "Detail \(counter)")
+ }
+
+ func push(_ fragment: AndroidApp.Fragment, name: String) {
+ log("\(self).\(#function) \(name)")
+ _ = getFragmentManager()
+ .beginTransaction()
+ .replace(rootViewID, fragment)
+ .addToBackStack(name)
+ .commit()
+ }
+
+ func setListView(_ items: [String]) {
+ let layout = try! JavaClass()
+ let resource = layout.simple_list_item_1
+ assert(resource != 0)
+ let objects: [JavaObject?] = items.map { JavaString($0) }
+ let adapter = ArrayAdapter(
+ context: self,
+ resource: resource,
+ objects: objects
+ )
+ listView.setAdapter(adapter.as(Adapter.self))
+ setRootView(listView)
+ }
+
+ func setRecyclerView() {
+ let items = [
+ "Row 1",
+ "Row 2",
+ "Row 3",
+ "Row 4",
+ "Row 5"
+ ]
+ let callback = RecyclerViewAdapter.Callback(
+ onBindViewHolder: { (holder, position) in
+ guard let viewHolder = holder.as(RecyclerViewAdapter.ViewHolder.self) else {
+ return
+ }
+ // get view
+ let linearLayout = viewHolder.itemView.as(LinearLayout.self)!
+ let textView: TextView
+ if linearLayout.getChildCount() == 0 {
+ textView = TextView(self)
+ linearLayout.addView(textView)
+ } else {
+ textView = linearLayout.getChildAt(0).as(TextView.self)!
+ }
+ // set data
+ let data = items[Int(position)]
+ textView.text = data
+ },
+ getItemCount: {
+ Int32(items.count)
+ }
+ )
+ let adapter = RecyclerViewAdapter(callback)
+ recyclerView.setLayoutManager(LinearLayoutManager(self))
+ recyclerView.setAdapter(adapter)
+ setRootView(recyclerView)
+ }
+
+ @MainActor
+ func updateTextView() {
+ log("\(self).\(#function)")
+ let results = results.keys.sorted().map { $0.description }
+ var text = "Hello Swift!\n\(Date().formatted(date: .numeric, time: .complete))"
+ for result in results {
+ text += "\n\(result)"
+ }
+ textView.text = text
+ }
+
+ func setTabBar() {
+ let layout = LinearLayout(self)
+ layout.orientation = .vertical
+
+ let container = FrameLayout(self)
+ container.setId(2001)
+
+ let bottomNav = BottomNavigationView(self)
+ _ = bottomNav.getMenu().add(0, 1, 0, JavaString("Home").as(CharSequence.self)).setIcon(17301543)
+ _ = bottomNav.getMenu().add(0, 2, 1, JavaString("Profile").as(CharSequence.self)).setIcon(17301659)
+
+ let homeFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+ let label = TextView(context)
+ label.text = "Home View"
+ label.gravity = .center
+ view.as(ViewGroup.self)!.addView(label)
+ }))
+
+ let profileFragment = Fragment(callback: .init(onViewCreated: { view, bundle in
+ let context = self
+ let label = TextView(context)
+ label.text = "Profile"
+ label.gravity = .center
+ view.as(ViewGroup.self)!.addView(label)
+ }))
+
+ let fragment1 = homeFragment
+ let fragment2 = profileFragment
+
+ let listener = NavigationBarViewOnItemSelectedListener { item in
+ guard let item else { return false }
+ let fragment: AndroidApp.Fragment = (item.getItemId() == 1) ? fragment1 : fragment2
+ _ = self.getFragmentManager().beginTransaction()
+ .replace(2001, fragment)
+ .commit()
+ return true
+ }
+ bottomNav.setOnItemSelectedListener(listener.as(NavigationBarView.OnItemSelectedListener.self))
+
+ let matchParent = try! JavaClass().MATCH_PARENT
+ let wrapContent = try! JavaClass().WRAP_CONTENT
+
+ layout.addView(container as AndroidView.View, ViewGroup.LayoutParams(matchParent, 1))
+ layout.addView(bottomNav as AndroidView.View, ViewGroup.LayoutParams(matchParent, wrapContent))
+
+ self.setRootView(layout)
+
+ // Default to Home
+ _ = self.getFragmentManager().beginTransaction()
+ .add(2001, fragment1)
+ .commit()
+ }
+
+ #if canImport(Binder)
+ private func printBinderVersion() {
+ // Print Binder version
+ do {
+ let version = try BinderVersion.current
+ logInfo("Binder Version: \(version)")
+ }
+ catch {
+ logError("Unable to read binder: \(error)")
+ }
+ }
+ #endif
+}
+
+extension MainActivity {
+
+ static var logTag: LogTag { "MainActivity" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+
+ func logInfo(_ string: String) {
+ Self.logInfo(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/NavigationBarViewOnItemSelectedListener.swift b/Demo/app/src/main/swift/NavigationBarViewOnItemSelectedListener.swift
new file mode 100644
index 0000000..96670b6
--- /dev/null
+++ b/Demo/app/src/main/swift/NavigationBarViewOnItemSelectedListener.swift
@@ -0,0 +1,76 @@
+//
+// OnItemSelectedListener.swift
+// SwiftAndroidApp
+//
+// Created by Alsey Coleman Miller on 6/21/25.
+//
+
+import Foundation
+import AndroidKit
+import AndroidMaterial
+
+@JavaClass("com.pureswift.swiftandroid.NavigationBarViewOnItemSelectedListener", extends: AndroidMaterial.NavigationView.OnNavigationItemSelectedListener.self)
+open class NavigationBarViewOnItemSelectedListener: JavaObject {
+
+ public typealias Action = (MenuItem?) -> (Bool)
+
+ @JavaMethod
+ @_nonoverride public convenience init(action: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getAction() -> SwiftObject?
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.NavigationBarViewOnItemSelectedListener")
+extension NavigationBarViewOnItemSelectedListener {
+
+ @JavaMethod
+ func onNavigationItemSelected(menuItem: MenuItem?) -> Bool {
+ log("\(self).\(#function)")
+ // drain queue
+ RunLoop.main.run(until: Date() + 0.01)
+ let result = action(menuItem)
+ RunLoop.main.run(until: Date() + 0.01)
+ return result
+ }
+}
+
+public extension NavigationBarViewOnItemSelectedListener {
+
+ convenience init(action: @escaping Action, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(action, environment: environment)
+ self.init(action: object, environment: environment)
+ }
+
+ var action: Action {
+ getAction()!.valueObject().value as! Action
+ }
+}
+
+extension NavigationBarViewOnItemSelectedListener {
+
+ static var logTag: LogTag { "NavigationBarViewOnItemSelectedListener" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/OnClickListener.swift b/Demo/app/src/main/swift/OnClickListener.swift
new file mode 100644
index 0000000..c57c8fe
--- /dev/null
+++ b/Demo/app/src/main/swift/OnClickListener.swift
@@ -0,0 +1,74 @@
+//
+// OnClickListener.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import Foundation
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.ViewOnClickListener", extends: AndroidView.View.OnClickListener.self)
+open class ViewOnClickListener: JavaObject {
+
+ public typealias Action = () -> ()
+
+ @JavaMethod
+ @_nonoverride public convenience init(action: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getAction() -> SwiftObject?
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.ViewOnClickListener")
+extension ViewOnClickListener {
+
+ @JavaMethod
+ func onClick() {
+ log("\(self).\(#function)")
+ // drain queue
+ RunLoop.main.run(until: Date() + 0.01)
+ action()
+ RunLoop.main.run(until: Date() + 0.01)
+ }
+}
+
+public extension ViewOnClickListener {
+
+ convenience init(action: @escaping () -> (), environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(action, environment: environment)
+ self.init(action: object, environment: environment)
+ }
+
+ var action: (() -> ()) {
+ getAction()!.valueObject().value as! Action
+ }
+}
+
+extension ViewOnClickListener {
+
+ static var logTag: LogTag { "ViewOnClickListener" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
diff --git a/Demo/app/src/main/swift/RecyclerView.swift b/Demo/app/src/main/swift/RecyclerView.swift
new file mode 100644
index 0000000..4bd25e2
--- /dev/null
+++ b/Demo/app/src/main/swift/RecyclerView.swift
@@ -0,0 +1,107 @@
+//
+// RecyclerView.swift
+// SwiftAndroidApp
+//
+// Created by Alsey Coleman Miller on 6/13/25.
+//
+
+import AndroidKit
+
+@JavaClass("com.pureswift.swiftandroid.RecyclerViewAdapter")
+open class RecyclerViewAdapter: RecyclerView.Adapter {
+
+ @JavaMethod
+ @_nonoverride public convenience init(swiftObject: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ func getSwiftObject() -> SwiftObject!
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.RecyclerViewAdapter")
+extension RecyclerViewAdapter {
+
+ @JavaMethod
+ public func onBindViewHolderSwift(_ viewHolder: RecyclerViewAdapter.ViewHolder?, _ position: Int32) {
+ log("\(self).\(#function) \(position)")
+ callback.onBindViewHolder(viewHolder!, position)
+ }
+
+ @JavaMethod
+ public func getItemCountSwift() -> Int32 {
+ log("\(self).\(#function)")
+ return callback.getItemCount()
+ }
+}
+
+extension RecyclerViewAdapter {
+
+ static var logTag: LogTag { "RecyclerViewAdapter" }
+
+ static func log(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .debug)
+ .log(string)
+ }
+
+ static func logInfo(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .info)
+ .log(string)
+ }
+
+ static func logError(_ string: String) {
+ try? AndroidLogger(tag: logTag, priority: .error)
+ .log(string)
+ }
+
+ func log(_ string: String) {
+ Self.log(string)
+ }
+
+ func logError(_ string: String) {
+ Self.logError(string)
+ }
+}
+
+public extension RecyclerViewAdapter {
+
+ struct Callback {
+
+ var onBindViewHolder: ((RecyclerViewAdapter.ViewHolder, Int32) -> ())
+
+ var getItemCount: () -> Int32
+
+ public init(
+ onBindViewHolder: @escaping ((RecyclerViewAdapter.ViewHolder, Int32) -> Void),
+ getItemCount: @escaping () -> Int32 = { return 0 }
+ ) {
+ self.onBindViewHolder = onBindViewHolder
+ self.getItemCount = getItemCount
+ }
+ }
+}
+
+public extension RecyclerViewAdapter {
+
+ convenience init(_ callback: Callback, environment: JNIEnvironment? = nil) {
+ let swiftObject = SwiftObject(callback, environment: environment)
+ self.init(swiftObject: swiftObject, environment: environment)
+ }
+
+ var callback: Callback {
+ get {
+ getSwiftObject().valueObject().value as! Callback
+ }
+ set {
+ getSwiftObject().valueObject().value = newValue
+ }
+ }
+}
+
+extension RecyclerViewAdapter {
+
+ @JavaClass("com.pureswift.swiftandroid.RecyclerViewAdapter$ViewHolder")
+ open class ViewHolder: RecyclerView.ViewHolder {
+
+ @JavaMethod
+ @_nonoverride public convenience init(view: AndroidView.View?, environment: JNIEnvironment? = nil)
+ }
+}
diff --git a/Demo/app/src/main/swift/Runnable.swift b/Demo/app/src/main/swift/Runnable.swift
new file mode 100644
index 0000000..946a138
--- /dev/null
+++ b/Demo/app/src/main/swift/Runnable.swift
@@ -0,0 +1,50 @@
+//
+// Runnable.swift
+// AndroidSwiftUI
+//
+// Created by Alsey Coleman Miller on 6/9/25.
+//
+
+import SwiftJava
+import CSwiftJavaJNI
+import AndroidKit
+import JavaLang
+
+@JavaClass("com.pureswift.swiftandroid.Runnable", extends: JavaLang.Runnable.self)
+open class Runnable: JavaObject {
+
+ public typealias Block = () -> ()
+
+ @JavaMethod
+ @_nonoverride public convenience init(block: SwiftObject?, environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ public func getBlock() -> SwiftObject?
+}
+
+public extension Runnable {
+
+ convenience init(_ block: @escaping () -> Void, environment: JNIEnvironment? = nil) {
+ let object = SwiftObject(block, environment: environment)
+ self.init(block: object, environment: environment)
+ }
+}
+
+@JavaImplementation("com.pureswift.swiftandroid.Runnable")
+extension Runnable {
+
+ @JavaMethod
+ func run() {
+ block()
+ }
+}
+
+private extension Runnable {
+
+ var block: Block {
+ guard let block = getBlock()?.valueObject().value as? Block else {
+ fatalError()
+ }
+ return block
+ }
+}
diff --git a/Demo/app/src/main/swift/ThreadSafeHelperClass.swift b/Demo/app/src/main/swift/ThreadSafeHelperClass.swift
new file mode 100644
index 0000000..82efbb5
--- /dev/null
+++ b/Demo/app/src/main/swift/ThreadSafeHelperClass.swift
@@ -0,0 +1,54 @@
+// Auto-generated by Java-to-Swift wrapper generator.
+import SwiftJava
+import CSwiftJavaJNI
+
+@JavaClass("com.example.swift.ThreadSafeHelperClass")
+open class ThreadSafeHelperClass: JavaObject {
+ @JavaField(isFinal: false)
+ public var text: JavaOptional!
+
+
+ public var textOptional: JavaString? {
+ get {
+ Optional(javaOptional: text)
+ }
+ set {
+ text = newValue.toJavaOptional()
+ }
+ }
+
+ @JavaField(isFinal: true)
+ public var val: JavaOptionalDouble!
+
+
+ public var valOptional: Double? {
+ get {
+ Optional(javaOptional: val)
+ }
+ }
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ open func getValue(_ name: JavaOptional?) -> String
+
+ open func getValueOptional(_ name: JavaString?) -> String {
+ getValue(name.toJavaOptional())
+ }
+
+ @JavaMethod
+ open func from(_ value: JavaOptionalInt?) -> JavaOptionalLong!
+
+ open func fromOptional(_ value: Int32?) -> Int64? {
+ Optional(javaOptional: from(value.toJavaOptional()))
+ }
+
+ @JavaMethod
+ open func getText() -> JavaOptional!
+
+ open func getTextOptional() -> JavaString? {
+ Optional(javaOptional: getText())
+ }
+}
+extension ThreadSafeHelperClass: @unchecked Swift.Sendable { }
diff --git a/Demo/app/src/main/swift/UnitEmitter.swift b/Demo/app/src/main/swift/UnitEmitter.swift
new file mode 100644
index 0000000..972b906
--- /dev/null
+++ b/Demo/app/src/main/swift/UnitEmitter.swift
@@ -0,0 +1,19 @@
+//
+// UnitEmitter.swift
+// SwiftAndroidApp
+//
+// Created by Alsey Coleman Miller on 7/13/25.
+//
+
+import SwiftJava
+
+/// Bridge from Swift to Kotlin Coroutines
+@JavaClass("com.pureswift.swiftandroid.UnitEmitter")
+open class UnitEmitter: JavaObject {
+
+ @JavaMethod
+ @_nonoverride public convenience init(environment: JNIEnvironment? = nil)
+
+ @JavaMethod
+ func emit()
+}
diff --git a/Demo/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt b/Demo/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt
new file mode 100644
index 0000000..2d1f328
--- /dev/null
+++ b/Demo/app/src/test/java/com/pureswift/swiftandroid/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.pureswift.swiftandroid
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/Demo/build-swift.sh b/Demo/build-swift.sh
new file mode 100755
index 0000000..c66f135
--- /dev/null
+++ b/Demo/build-swift.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/swift-define"
+JNI_LIBS_DIR="$SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH"
+
+# Build with SwiftPM
+ANDROID_NDK_ROOT="" ANDROID_SDK_VERSION="$ANDROID_SDK_VERSION" skip android build --arch "$SWIFT_TARGET_ARCH" --android-api-level "$ANDROID_SDK_VERSION"
+
+# Copy compiled Swift package
+mkdir -p "$JNI_LIBS_DIR/"
+cp -f "$SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug/libSwiftAndroidApp.so" "$JNI_LIBS_DIR/"
+
+# Copy Swift runtime shared libraries required by libSwiftAndroidApp.so.
+if [[ -d "$SWIFT_ANDROID_RUNTIME_LIBS" ]]; then
+ shopt -s nullglob
+ for so in "$SWIFT_ANDROID_RUNTIME_LIBS"/*.so; do
+ cp -f "$so" "$JNI_LIBS_DIR/"
+ done
+ shopt -u nullglob
+fi
+
+# Copy SwiftJava helper library when available.
+if [[ -f "$SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug/libSwiftJava.so" ]]; then
+ cp -f "$SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug/libSwiftJava.so" "$JNI_LIBS_DIR/"
+fi
+
+# Copy C++ runtime from Android sysroot.
+if [[ -f "$SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so" ]]; then
+ cp -f "$SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so" "$JNI_LIBS_DIR/"
+fi
diff --git a/Demo/build.gradle.kts b/Demo/build.gradle.kts
new file mode 100644
index 0000000..952b930
--- /dev/null
+++ b/Demo/build.gradle.kts
@@ -0,0 +1,6 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+}
\ No newline at end of file
diff --git a/Demo/gradle.properties b/Demo/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/Demo/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/Demo/gradle/libs.versions.toml b/Demo/gradle/libs.versions.toml
new file mode 100644
index 0000000..792411d
--- /dev/null
+++ b/Demo/gradle/libs.versions.toml
@@ -0,0 +1,38 @@
+[versions]
+agp = "8.10.1"
+kotlin = "2.0.21"
+coreKtx = "1.10.1"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+lifecycleRuntimeKtx = "2.6.1"
+activityCompose = "1.8.0"
+composeBom = "2024.09.00"
+material = "1.12.0"
+navigationRuntime = "2.9.0"
+recyclerview = "1.4.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "navigationRuntime" }
+androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+material = { module = "com.google.android.material:material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+
diff --git a/Demo/gradle/wrapper/gradle-wrapper.properties b/Demo/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3473e96
--- /dev/null
+++ b/Demo/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Jun 07 21:03:00 EDT 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Demo/gradlew b/Demo/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/Demo/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/Demo/gradlew.bat b/Demo/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/Demo/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Demo/local.properties b/Demo/local.properties
new file mode 100644
index 0000000..e018a92
--- /dev/null
+++ b/Demo/local.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/Users/coleman/Library/Android/sdk
\ No newline at end of file
diff --git a/Demo/settings.gradle.kts b/Demo/settings.gradle.kts
new file mode 100644
index 0000000..4447e1b
--- /dev/null
+++ b/Demo/settings.gradle.kts
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "SwiftAndroid"
+include(":app")
diff --git a/Demo/setup.sh b/Demo/setup.sh
new file mode 100755
index 0000000..90b1824
--- /dev/null
+++ b/Demo/setup.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/swift-define"
+JNI_LIBS_DIR="$SRC_ROOT/app/src/main/jniLibs/$ANDROID_ARCH"
+
+# Install macOS dependencies
+if [[ $OSTYPE == 'darwin'* ]]; then
+ echo "Install macOS build dependencies"
+ brew install skiptools/skip/skip
+ skip android sdk install
+ brew update
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install wget cmake ninja android-ndk
+fi
+
+# Copy Swift libraries
+rm -rf "$JNI_LIBS_DIR/"
+mkdir -p "$JNI_LIBS_DIR/"
+
+copied_swift_libs=0
+if [[ -d "$SWIFT_ANDROID_RUNTIME_LIBS" ]]; then
+ shopt -s nullglob
+ for so in "$SWIFT_ANDROID_RUNTIME_LIBS"/*.so; do
+ cp -f "$so" "$JNI_LIBS_DIR/"
+ copied_swift_libs=1
+ done
+ shopt -u nullglob
+fi
+
+# Fallback for newer Skip/Swift SDK layouts where runtime libs are emitted into `.build`.
+if [[ $copied_swift_libs -eq 0 && -d "$SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug" ]]; then
+ shopt -s nullglob
+ for so in "$SWIFT_PACKAGE_SRC/.build/$SWIFT_TARGET_NAME/debug"/libSwift*.so; do
+ cp -f "$so" "$JNI_LIBS_DIR/"
+ copied_swift_libs=1
+ done
+ shopt -u nullglob
+fi
+
+if [[ $copied_swift_libs -eq 0 ]]; then
+ echo "Warning: No Swift runtime libraries found to copy."
+fi
+
+# Copy C stdlib
+if [[ -f "$SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so" ]]; then
+ cp -f "$SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so" \
+ "$JNI_LIBS_DIR/"
+else
+ echo "Warning: libc++_shared.so not found at $SWIFT_ANDROID_SYSROOT/usr/lib/$ANDROID_LIB/libc++_shared.so"
+fi
+echo "Copied Swift libraries"
diff --git a/Demo/swift-define b/Demo/swift-define
new file mode 100644
index 0000000..43004e4
--- /dev/null
+++ b/Demo/swift-define
@@ -0,0 +1,30 @@
+# Configurable
+SWIFT_DEFINE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SRC_ROOT="${SRC_ROOT:=$SWIFT_DEFINE_DIR}"
+SWIFT_TARGET_ARCH="${SWIFT_TARGET_ARCH:=aarch64}"
+ANDROID_ARCH="${ANDROID_ARCH:=arm64-v8a}"
+ANDROID_LIB="${ANDROID_LIB:=aarch64-linux-android}"
+SWIFT_COMPILATION_MODE="${SWIFT_COMPILATION_MODE:=debug}"
+
+# Version
+ANDROID_SDK_VERSION=28
+SWIFT_VERSION_SHORT=6.2.3
+SWIFT_VERSION=swift-$SWIFT_VERSION_SHORT-RELEASE
+SWIFT_TARGET_NAME=$SWIFT_TARGET_ARCH-unknown-linux-android$ANDROID_SDK_VERSION
+XCTOOLCHAIN=/Library/Developer/Toolchains/$SWIFT_VERSION.xctoolchain
+SWIFT_ARTIFACT_BUNDLE="${SWIFT_ARTIFACT_BUNDLE:=swift-$SWIFT_VERSION_SHORT-RELEASE_android.artifactbundle}"
+SWIFT_SDKS_ROOT="${SWIFT_SDKS_ROOT:=$HOME/Library/org.swift.swiftpm/swift-sdks}"
+if [[ ! -d "$SWIFT_SDKS_ROOT/$SWIFT_ARTIFACT_BUNDLE" ]]; then
+ SWIFT_SDKS_ROOT="$HOME/.swiftpm/swift-sdks"
+fi
+
+# Paths
+SWIFT_SDK=swift-$SWIFT_VERSION_SHORT-release-android-$ANDROID_SDK_VERSION-sdk
+SWIFT_ANDROID_SYSROOT="$SWIFT_SDKS_ROOT/$SWIFT_ARTIFACT_BUNDLE/swift-android/ndk-sysroot"
+SWIFT_ANDROID_LIBS="$SWIFT_SDKS_ROOT/$SWIFT_ARTIFACT_BUNDLE/swift-android/swift-resources/usr/lib/swift-$SWIFT_TARGET_ARCH"
+SWIFT_ANDROID_RUNTIME_LIBS="$SWIFT_ANDROID_LIBS/android"
+SWIFT_PACKAGE_SRC=$SRC_ROOT
+JAVA_HOME=$SWIFT_ANDROID_SYSROOT/usr
+
+# Configurable
+SWIFT_NATIVE_PATH="${SWIFT_NATIVE_PATH:=$XCTOOLCHAIN/usr/bin}"
diff --git a/Sources/AndroidBluetooth/AndroidCentralCallback.swift b/Sources/AndroidBluetooth/AndroidCentralCallback.swift
index ec6ed8a..3c01f66 100644
--- a/Sources/AndroidBluetooth/AndroidCentralCallback.swift
+++ b/Sources/AndroidBluetooth/AndroidCentralCallback.swift
@@ -64,17 +64,25 @@ private extension AndroidCentral.LowEnergyScanCallback {
extension AndroidCentral.LowEnergyScanCallback {
@JavaMethod
- func Swift_release(_ swiftPeer: Int64) {
+ func swiftRelease(_ swiftPeer: Int64) {
setSwiftPeer(0)
}
- @JavaMethod("Swift_onScanResult")
- func onScanResult(_ swiftPeer: Int64, error: Int32, result: AndroidBluetooth.ScanResult?) {
- guard let central = central(swiftPeer), let result, let scanData = try? ScanData(result) else {
+ @JavaMethod
+ func swiftOnScanResult(
+ _ swiftPeer: Int64,
+ error: Int32,
+ result: AndroidBluetooth.ScanResult?
+ ) {
+ guard let central = central(swiftPeer),
+ let result,
+ let scanData = try? ScanData(result) else {
assertionFailure()
return
}
- central.log?("\(type(of: self)): \(#function) name: \(try? result.getDevice().getName() ?? "") address: \(try? result.getDevice().getAddress())")
+ //
+ central.log?("\(type(of: self)): \(#function) name: \((try? result.getDevice().getName()) ?? "") address: \(result.getDevice().getAddress())")
+
Task {
await central.storage.update { state in
state.scan.continuation?.yield(scanData)
@@ -87,7 +95,7 @@ extension AndroidCentral.LowEnergyScanCallback {
}
@JavaMethod
- func onBatchScanResults(_ swiftPeer: Int64, results: [AndroidBluetooth.ScanResult?]) {
+ func swiftOnBatchScanResults(_ swiftPeer: Int64, results: [AndroidBluetooth.ScanResult?]) {
guard let central = central(swiftPeer) else {
return
}
@@ -110,7 +118,7 @@ extension AndroidCentral.LowEnergyScanCallback {
}
@JavaMethod
- func onScanFailedSwift(_ swiftPeer: Int64, error: Int32) {
+ func swiftOnScanFailed(_ swiftPeer: Int64, error: Int32) {
let central = central(swiftPeer)
central?.log?("\(type(of: self)): \(#function)")