diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..316d436
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+jniLibs
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..c8bc73b
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a48f90d..b0b4a5f 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,15 @@
-# swift-android-examples
\ No newline at end of file
+# Swift Android Examples
+
+This repository contains sample apps that use the [Swift Android SDK](https://www.swift.org/).
+
+# Build and run
+
+1. Setup Swift Android SDK
+2. Clone this repository
+3. Open the whole project in Android Studio
+4. Select the sample you want to run in the top bar (you may need to sync gradle first)
+5. Click the play button to run the sample
+
+You can also build the samples from the command line if you prefer. Use `./gradlew` build to build everything. For individual tasks, see `./gradlew tasks`. To see the tasks for an individual sample, run the tasks task for that directory. For example, `./gradlew :hello-swift:tasks` will show the tasks for the hello-swift app.
+
+You can build all sample apps for both the debug and release build types by running `./gradlew assemble`.
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..ecf7a23
--- /dev/null
+++ b/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.android.library) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..20e2a01
--- /dev/null
+++ b/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/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..b34d78a
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,25 @@
+[versions]
+agp = "8.13.0"
+kotlin = "2.0.21"
+coreKtx = "1.16.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.12.0"
+constraintlayout = "2.2.1"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+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-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..b30abe8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Aug 09 13:51:11 EEST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..8b35b5b
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,169 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## 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/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..6b3da4b
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,73 @@
+@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/hello-swift-callback/.gitignore b/hello-swift-callback/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/hello-swift-callback/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/hello-swift-callback/build.gradle.kts b/hello-swift-callback/build.gradle.kts
new file mode 100644
index 0000000..4ff1f49
--- /dev/null
+++ b/hello-swift-callback/build.gradle.kts
@@ -0,0 +1,48 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+apply(from = "../swift-android.gradle.kts")
+
+android {
+ namespace = "org.example.helloswift"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "org.example.helloswift"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ debug {
+ isJniDebuggable = true
+ }
+ release {
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ lint {
+ checkReleaseBuilds = false
+ abortOnError = false
+ }
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.constraintlayout)
+}
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/AndroidManifest.xml b/hello-swift-callback/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..92ce8a8
--- /dev/null
+++ b/hello-swift-callback/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/java/org/example/helloswift/MainActivity.kt b/hello-swift-callback/src/main/java/org/example/helloswift/MainActivity.kt
new file mode 100644
index 0000000..e60f4a4
--- /dev/null
+++ b/hello-swift-callback/src/main/java/org/example/helloswift/MainActivity.kt
@@ -0,0 +1,80 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+package org.example.helloswift
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.annotation.Keep
+import androidx.appcompat.app.AppCompatActivity
+import java.util.Locale
+
+class MainActivity : AppCompatActivity() {
+
+ var hour: Int = 0
+ var minute: Int = 0
+ var second: Int = 0
+ var tickView: TextView? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ tickView = findViewById(R.id.tickView)
+ }
+
+ public override fun onResume() {
+ super.onResume()
+ second = 0
+ minute = 0
+ hour = 0
+ startTicks()
+ }
+
+ public override fun onPause() {
+ super.onPause()
+ stopTicks()
+ }
+
+ /*
+ * A function calling from JNI to update current timer
+ */
+ @Keep
+ private fun updateTimer() {
+ ++second
+ if (second >= 60) {
+ ++minute
+ second -= 60
+ if (minute >= 60) {
+ ++hour
+ minute -= 60
+ }
+ }
+ runOnUiThread {
+ val ticks = String.format(
+ Locale.ENGLISH,
+ "%02d:%02d:%02d",
+ this@MainActivity.hour, this@MainActivity.minute, this@MainActivity.second
+ )
+ this@MainActivity.tickView?.text = ticks
+ }
+ }
+
+ external fun startTicks()
+ external fun stopTicks()
+
+ companion object {
+ // Used to load the 'hello-swift-callback' library on application startup.
+ init {
+ System.loadLibrary("hello-swift-callback")
+ }
+ }
+}
diff --git a/hello-swift-callback/src/main/res/drawable/ic_launcher_background.xml b/hello-swift-callback/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/hello-swift-callback/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hello-swift-callback/src/main/res/drawable/ic_launcher_foreground.xml b/hello-swift-callback/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/hello-swift-callback/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/layout/activity_main.xml b/hello-swift-callback/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..ad22037
--- /dev/null
+++ b/hello-swift-callback/src/main/res/layout/activity_main.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher.xml b/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/hello-swift-callback/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher.webp b/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher.webp b/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher.webp b/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/hello-swift-callback/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift-callback/src/main/res/values-night/themes.xml b/hello-swift-callback/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..40983c3
--- /dev/null
+++ b/hello-swift-callback/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/values/colors.xml b/hello-swift-callback/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/hello-swift-callback/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/hello-swift-callback/src/main/res/values/strings.xml b/hello-swift-callback/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fd9cc30
--- /dev/null
+++ b/hello-swift-callback/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Hello Swift
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/values/themes.xml b/hello-swift-callback/src/main/res/values/themes.xml
new file mode 100644
index 0000000..247c9ae
--- /dev/null
+++ b/hello-swift-callback/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/xml/backup_rules.xml b/hello-swift-callback/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/hello-swift-callback/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/res/xml/data_extraction_rules.xml b/hello-swift-callback/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/hello-swift-callback/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-callback/src/main/swift/.gitignore b/hello-swift-callback/src/main/swift/.gitignore
new file mode 100644
index 0000000..0023a53
--- /dev/null
+++ b/hello-swift-callback/src/main/swift/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/hello-swift-callback/src/main/swift/Package.swift b/hello-swift-callback/src/main/swift/Package.swift
new file mode 100644
index 0000000..792ce4e
--- /dev/null
+++ b/hello-swift-callback/src/main/swift/Package.swift
@@ -0,0 +1,14 @@
+// swift-tools-version: 5.10
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "hello-swift-callback",
+ products: [
+ .library(name: "hello-swift-callback", type: .dynamic, targets: ["hello-swift-callback"]),
+ ],
+ targets: [
+ .target(name: "hello-swift-callback")
+ ]
+)
diff --git a/hello-swift-callback/src/main/swift/Sources/hello-swift-callback/hello-swift-callback.swift b/hello-swift-callback/src/main/swift/Sources/hello-swift-callback/hello-swift-callback.swift
new file mode 100644
index 0000000..cd20234
--- /dev/null
+++ b/hello-swift-callback/src/main/swift/Sources/hello-swift-callback/hello-swift-callback.swift
@@ -0,0 +1,80 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Android
+import Foundation
+import Dispatch
+
+private var gJavaVM: UnsafeMutablePointer?
+
+@_cdecl("JNI_OnLoad")
+public func JNI_OnLoad(vm: UnsafeMutablePointer, reserved: UnsafeMutableRawPointer?) -> jint {
+ gJavaVM = vm
+ return jint(JNI_VERSION_1_6)
+}
+
+struct JGlobalObject: @unchecked Sendable {
+ let ref: jobject
+}
+
+struct JMethodID: @unchecked Sendable {
+ let id: jmethodID
+}
+
+let queue = DispatchQueue(label: "hello-swift-callback")
+var workItem: DispatchWorkItem? = nil
+var activityRef: jobject? = nil
+
+private func getEnvForCurrentThread(block: (UnsafeMutablePointer?) -> Void) {
+ var env: UnsafeMutablePointer?
+ let attachCode = gJavaVM!.pointee!.pointee.AttachCurrentThread(gJavaVM, &env, nil)
+ guard attachCode == 0 else { return }
+ block(env)
+ _ = gJavaVM!.pointee!.pointee.DetachCurrentThread(gJavaVM)
+}
+
+@_cdecl("Java_org_example_helloswift_MainActivity_startTicks")
+public func MainActivity_startTicks(env: UnsafeMutablePointer, thiz: jobject) {
+ guard let globalRef = env.pointee!.pointee.NewGlobalRef(env, thiz) else { return }
+ guard let cls = env.pointee!.pointee.GetObjectClass(env, thiz) else { return }
+ defer { env.pointee!.pointee.DeleteLocalRef(env, cls) }
+ guard let mid = env.pointee!.pointee.GetMethodID(env, cls, "updateTimer", "()V") else { return }
+
+ let activityHandle = JGlobalObject(ref: globalRef)
+ let methodHandle = JMethodID(id: mid)
+
+ queue.async {
+ workItem?.cancel()
+ workItem = DispatchWorkItem {
+ getEnvForCurrentThread { env in
+ env?.pointee!.pointee.CallVoidMethodA(env, activityHandle.ref, methodHandle.id, nil)
+ }
+ if let workItem = workItem, workItem.isCancelled == false {
+ queue.asyncAfter(deadline: .now() + 1, execute: workItem)
+ }
+ }
+ queue.async(execute: workItem!)
+ }
+}
+
+@_cdecl("Java_org_example_helloswift_MainActivity_stopTicks")
+public func MainActivity_stopTicks(env: UnsafeMutablePointer, jthis: jobject) {
+ queue.async {
+ workItem?.cancel()
+ workItem = nil
+ if let activityRef = activityRef {
+ getEnvForCurrentThread { env in
+ env?.pointee!.pointee.DeleteGlobalRef(env, activityRef)
+ }
+ }
+ }
+}
diff --git a/hello-swift-library/.gitignore b/hello-swift-library/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/hello-swift-library/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/hello-swift-library/build.gradle.kts b/hello-swift-library/build.gradle.kts
new file mode 100644
index 0000000..d81a9f1
--- /dev/null
+++ b/hello-swift-library/build.gradle.kts
@@ -0,0 +1,45 @@
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+}
+
+apply(from = "../swift-android.gradle.kts")
+
+android {
+ namespace = "org.example.library"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 29
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ debug {
+ isJniDebuggable = true
+ }
+ release {
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+}
+
+dependencies {
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+}
\ No newline at end of file
diff --git a/hello-swift-library/src/androidTest/java/org/example/swiftlibrary/SwiftLibraryTest.kt b/hello-swift-library/src/androidTest/java/org/example/swiftlibrary/SwiftLibraryTest.kt
new file mode 100644
index 0000000..a70905f
--- /dev/null
+++ b/hello-swift-library/src/androidTest/java/org/example/swiftlibrary/SwiftLibraryTest.kt
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+package org.example.swiftlibrary
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class SwiftLibraryTest {
+ @Test
+ fun testLibrary() {
+ assertEquals("Hello from Swift", SwiftLibrary().stringFromSwift())
+ }
+}
diff --git a/hello-swift-library/src/main/AndroidManifest.xml b/hello-swift-library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a5918e6
--- /dev/null
+++ b/hello-swift-library/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift-library/src/main/java/org/example/swiftlibrary/SwiftLibrary.kt b/hello-swift-library/src/main/java/org/example/swiftlibrary/SwiftLibrary.kt
new file mode 100644
index 0000000..c72a86c
--- /dev/null
+++ b/hello-swift-library/src/main/java/org/example/swiftlibrary/SwiftLibrary.kt
@@ -0,0 +1,24 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+package org.example.swiftlibrary
+
+class SwiftLibrary {
+
+ external fun stringFromSwift(): String
+
+ companion object {
+ init {
+ System.loadLibrary("hello-swift-library")
+ }
+ }
+}
diff --git a/hello-swift-library/src/main/swift/.gitignore b/hello-swift-library/src/main/swift/.gitignore
new file mode 100644
index 0000000..0023a53
--- /dev/null
+++ b/hello-swift-library/src/main/swift/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/hello-swift-library/src/main/swift/Package.swift b/hello-swift-library/src/main/swift/Package.swift
new file mode 100644
index 0000000..52a4456
--- /dev/null
+++ b/hello-swift-library/src/main/swift/Package.swift
@@ -0,0 +1,14 @@
+// swift-tools-version: 6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "hello-swift-library",
+ products: [
+ .library(name: "hello-swift-library", type: .dynamic, targets: ["hello-swift-library"]),
+ ],
+ targets: [
+ .target(name: "hello-swift-library")
+ ]
+)
diff --git a/hello-swift-library/src/main/swift/Sources/helloswift/helloswift.swift b/hello-swift-library/src/main/swift/Sources/helloswift/helloswift.swift
new file mode 100644
index 0000000..d943e8e
--- /dev/null
+++ b/hello-swift-library/src/main/swift/Sources/helloswift/helloswift.swift
@@ -0,0 +1,21 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Android
+
+@_cdecl("Java_org_example_swiftlibrary_SwiftLibrary_stringFromSwift")
+public func SwiftLibrary_stringFromSwift(env: UnsafeMutablePointer, clazz: jclass) -> jstring {
+ let hello = "Hello from Swift"
+ return hello.withCString { ptr in
+ env.pointee!.pointee.NewStringUTF(env, ptr)!
+ }
+}
diff --git a/hello-swift/.gitignore b/hello-swift/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/hello-swift/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/hello-swift/build.gradle.kts b/hello-swift/build.gradle.kts
new file mode 100644
index 0000000..2eb9582
--- /dev/null
+++ b/hello-swift/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+apply(from = "../swift-android.gradle.kts")
+
+android {
+ namespace = "org.example.helloswift"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "org.example.helloswift"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ debug {
+ isJniDebuggable = true
+ }
+ release {
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ lint {
+ checkReleaseBuilds = false
+ abortOnError = false
+ }
+}
+
+
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.constraintlayout)
+}
\ No newline at end of file
diff --git a/hello-swift/src/main/AndroidManifest.xml b/hello-swift/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..76dd877
--- /dev/null
+++ b/hello-swift/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/java/org/example/helloswift/MainActivity.kt b/hello-swift/src/main/java/org/example/helloswift/MainActivity.kt
new file mode 100644
index 0000000..c864dbf
--- /dev/null
+++ b/hello-swift/src/main/java/org/example/helloswift/MainActivity.kt
@@ -0,0 +1,39 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+package org.example.helloswift
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ findViewById(R.id.sample_text).text = stringFromSwift()
+ }
+
+ /**
+ * A native method that is implemented by the 'helloswift' native library,
+ * which is packaged with this application.
+ */
+ external fun stringFromSwift(): String
+
+ companion object {
+ // Used to load the 'helloswift' library on application startup.
+ init {
+ System.loadLibrary("helloswift")
+ }
+ }
+}
diff --git a/hello-swift/src/main/res/drawable/ic_launcher_background.xml b/hello-swift/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/hello-swift/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/hello-swift/src/main/res/drawable/ic_launcher_foreground.xml b/hello-swift/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/hello-swift/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/layout/activity_main.xml b/hello-swift/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..a73c8eb
--- /dev/null
+++ b/hello-swift/src/main/res/layout/activity_main.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/mipmap-anydpi/ic_launcher.xml b/hello-swift/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/hello-swift/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/hello-swift/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/hello-swift/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/mipmap-hdpi/ic_launcher.webp b/hello-swift/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/hello-swift/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/hello-swift/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/hello-swift/src/main/res/mipmap-mdpi/ic_launcher.webp b/hello-swift/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/hello-swift/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/hello-swift/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher.webp b/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/hello-swift/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/hello-swift/src/main/res/values-night/themes.xml b/hello-swift/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..40983c3
--- /dev/null
+++ b/hello-swift/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/values/colors.xml b/hello-swift/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f8c6127
--- /dev/null
+++ b/hello-swift/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/hello-swift/src/main/res/values/strings.xml b/hello-swift/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fd9cc30
--- /dev/null
+++ b/hello-swift/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Hello Swift
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/values/themes.xml b/hello-swift/src/main/res/values/themes.xml
new file mode 100644
index 0000000..247c9ae
--- /dev/null
+++ b/hello-swift/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/xml/backup_rules.xml b/hello-swift/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/hello-swift/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/res/xml/data_extraction_rules.xml b/hello-swift/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/hello-swift/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/hello-swift/src/main/swift/.gitignore b/hello-swift/src/main/swift/.gitignore
new file mode 100644
index 0000000..0023a53
--- /dev/null
+++ b/hello-swift/src/main/swift/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/hello-swift/src/main/swift/Package.swift b/hello-swift/src/main/swift/Package.swift
new file mode 100644
index 0000000..a039d87
--- /dev/null
+++ b/hello-swift/src/main/swift/Package.swift
@@ -0,0 +1,14 @@
+// swift-tools-version: 6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "helloswift",
+ products: [
+ .library(name: "helloswift", type: .dynamic, targets: ["helloswift"]),
+ ],
+ targets: [
+ .target(name: "helloswift")
+ ]
+)
diff --git a/hello-swift/src/main/swift/Sources/helloswift/helloswift.swift b/hello-swift/src/main/swift/Sources/helloswift/helloswift.swift
new file mode 100644
index 0000000..b7cd274
--- /dev/null
+++ b/hello-swift/src/main/swift/Sources/helloswift/helloswift.swift
@@ -0,0 +1,21 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Android
+
+@_cdecl("Java_org_example_helloswift_MainActivity_stringFromSwift")
+public func MainActivity_stringFromSwift(env: UnsafeMutablePointer, clazz: jclass) -> jstring {
+ let hello = ["Hello", "from", "Swift", "❤️"].joined(separator: " ")
+ return hello.withCString { ptr in
+ env.pointee!.pointee.NewStringUTF(env, ptr)!
+ }
+}
diff --git a/native-activity/.gitignore b/native-activity/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/native-activity/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/native-activity/build.gradle.kts b/native-activity/build.gradle.kts
new file mode 100644
index 0000000..a1c439a
--- /dev/null
+++ b/native-activity/build.gradle.kts
@@ -0,0 +1,29 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+}
+
+apply(from = "../swift-android.gradle.kts")
+
+android {
+ namespace = "org.example.nativeactivity"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "org.example.nativeactivity"
+ minSdk = 29
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ }
+
+ buildTypes {
+ debug {
+ isJniDebuggable = true
+ }
+ release {
+ isMinifyEnabled = false
+ isJniDebuggable = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/native-activity/src/main/AndroidManifest.xml b/native-activity/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d075f06
--- /dev/null
+++ b/native-activity/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/native-activity/src/main/res/mipmap-hdpi/ic_launcher.png b/native-activity/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/native-activity/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/native-activity/src/main/res/mipmap-mdpi/ic_launcher.png b/native-activity/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/native-activity/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/native-activity/src/main/res/mipmap-xhdpi/ic_launcher.png b/native-activity/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/native-activity/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/native-activity/src/main/res/mipmap-xxhdpi/ic_launcher.png b/native-activity/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/native-activity/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/native-activity/src/main/res/values/strings.xml b/native-activity/src/main/res/values/strings.xml
new file mode 100644
index 0000000..d8f5513
--- /dev/null
+++ b/native-activity/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ NativeActivity
+
diff --git a/native-activity/src/main/swift/.gitignore b/native-activity/src/main/swift/.gitignore
new file mode 100644
index 0000000..0023a53
--- /dev/null
+++ b/native-activity/src/main/swift/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/native-activity/src/main/swift/Package.swift b/native-activity/src/main/swift/Package.swift
new file mode 100644
index 0000000..eab71ff
--- /dev/null
+++ b/native-activity/src/main/swift/Package.swift
@@ -0,0 +1,29 @@
+// swift-tools-version: 6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "native-activity",
+ products: [
+ .library(name: "native-activity", type: .dynamic, targets: ["native-activity"]),
+ ],
+ dependencies: [
+ .package(path: "../../../../native-app-glue")
+ ],
+ targets: [
+ .target(
+ name: "native-activity",
+ dependencies: [
+ .product(name: "AndroidNativeAppGlue", package: "native-app-glue"),
+ .product(name: "AndroidOpenGL", package: "native-app-glue")
+ ],
+ linkerSettings: [
+ .linkedLibrary("android"),
+ .linkedLibrary("EGL"),
+ .linkedLibrary("GLESv1_CM"),
+ .linkedLibrary("log")
+ ]
+ )
+ ]
+)
diff --git a/native-activity/src/main/swift/Sources/native-activity/native-activity.swift b/native-activity/src/main/swift/Sources/native-activity/native-activity.swift
new file mode 100644
index 0000000..1dc27d0
--- /dev/null
+++ b/native-activity/src/main/swift/Sources/native-activity/native-activity.swift
@@ -0,0 +1,261 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See https://swift.org/LICENSE.txt for license information
+// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Android
+import AndroidNativeAppGlue
+import AndroidOpenGL
+
+public struct SavedState {
+ public var angle: Float = 0
+ public var x: Int32 = 0
+ public var y: Int32 = 0
+}
+
+public final class Engine {
+ public var app: UnsafeMutablePointer?
+
+ public var display: EGLDisplay?
+ public var surface: EGLSurface?
+ public var context: EGLContext?
+ public var width: Int32 = 0
+ public var height: Int32 = 0
+ public var state = SavedState()
+
+ private var running_ = false
+
+ public init(app: UnsafeMutablePointer?) {
+ self.app = app
+ }
+
+ /**
+ * Initialize an EGL context for the current display.
+ */
+ public func initDisplay() -> Int32 {
+ guard let win = app?.pointee.window else { return -1 }
+ // initialize OpenGL ES and EGL
+
+ /*
+ * Here specify the attributes of the desired configuration.
+ * Below, we select an EGLConfig with at least 8 bits per color
+ * component compatible with on-screen windows
+ */
+ let attribs: [EGLint] = [
+ EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
+ EGL_BLUE_SIZE, 8,
+ EGL_GREEN_SIZE, 8,
+ EGL_RED_SIZE, 8,
+ EGL_NONE
+ ]
+
+ let display: EGLDisplay! = eglGetDisplay(nil)
+ _ = eglInitialize(display, nil, nil)
+
+ var numConfigs: EGLint = 0
+ _ = eglChooseConfig(display, attribs, nil, 0, &numConfigs)
+ if numConfigs <= 0 { return -1 }
+ let cfgBuf = UnsafeMutablePointer.allocate(capacity: Int(numConfigs))
+ defer { cfgBuf.deallocate() }
+
+ /* Here, the application chooses the configuration it desires.
+ * find the best match if possible, otherwise use the very first one
+ */
+ _ = eglChooseConfig(display, attribs, cfgBuf, numConfigs, &numConfigs)
+
+ // Pick the best match: R=G=B=8, DEPTH=0
+ var chosen: EGLConfig? = nil
+ for i in 0.. 1 {
+ state.angle = 0
+ }
+ }
+
+ private func drawFrame() {
+ guard display != nil, surface != nil else {
+ // No display/surface yet.
+ return
+ }
+
+ // Just fill the screen with a color
+ let r = Float(state.x) / Float(max(1, width))
+ let g = Float(state.y) / Float(max(1, height))
+ let b = state.angle
+
+ glClearColor(r, g, b, 1)
+ glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
+
+ _ = eglSwapBuffers(display, surface)
+ }
+}
+
+@_cdecl("Engine_Tick")
+public func Engine_Tick(_ frameTimeNanos: Int, _ data: UnsafeMutableRawPointer?) {
+ guard let data else { return }
+ let engine = Unmanaged.fromOpaque(data).takeUnretainedValue()
+ engine.doTick()
+}
+
+@_silgen_name("android_main")
+public func android_main(_ app: UnsafeMutablePointer) {
+ let engine = Engine(app: app)
+ app.pointee.userData = Unmanaged.passRetained(engine).toOpaque()
+
+ app.pointee.onAppCmd = { app, cmd in
+ // unwrap the optional android_app* and userData
+ guard let opaque = app?.pointee.userData else { return }
+
+ // turn void* back into Engine
+ let engine = Unmanaged.fromOpaque(opaque).takeUnretainedValue()
+
+ switch Int(cmd) {
+ case APP_CMD_INIT_WINDOW:
+ _ = engine.initDisplay()
+ case APP_CMD_TERM_WINDOW:
+ engine.termDisplay()
+ case APP_CMD_GAINED_FOCUS:
+ engine.resume()
+ case APP_CMD_LOST_FOCUS:
+ engine.pause()
+ default:
+ print("Unsupported command \(cmd)")
+ }
+ }
+
+ while app.pointee.destroyRequested == 0 {
+ var outData: UnsafeMutableRawPointer? = nil
+
+ let r = ALooper_pollOnce(-1, nil, nil, &outData)
+ if r == ALOOPER_POLL_ERROR {
+ fatalError("ALooper_pollOnce returned an error")
+ }
+
+ if let outData {
+ let source = outData.assumingMemoryBound(to: android_poll_source.self)
+ source.pointee.process(app, source)
+ }
+ }
+
+ engine.termDisplay()
+}
diff --git a/native-app-glue/Package.swift b/native-app-glue/Package.swift
new file mode 100644
index 0000000..7a96a86
--- /dev/null
+++ b/native-app-glue/Package.swift
@@ -0,0 +1,17 @@
+// swift-tools-version:6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "AndroidNativeAppGlue",
+ products: [
+ .library(name: "AndroidNativeAppGlue", targets: ["AndroidNativeAppGlue"]),
+ .library(name: "AndroidOpenGL", targets: ["AndroidOpenGL"])
+ ],
+ targets: [
+ .target(name: "AndroidNativeAppGlue"),
+ .target(name: "AndroidOpenGL")
+ ],
+ cxxLanguageStandard: .cxx98
+)
diff --git a/native-app-glue/Sources/AndroidNativeAppGlue/android_native_app_glue.c b/native-app-glue/Sources/AndroidNativeAppGlue/android_native_app_glue.c
new file mode 100644
index 0000000..edd45dc
--- /dev/null
+++ b/native-app-glue/Sources/AndroidNativeAppGlue/android_native_app_glue.c
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.
+ */
+#include "android_native_app_glue.h"
+
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__))
+
+/* For debug builds, always enable the debug traces in this library */
+#ifndef NDEBUG
+# define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__))
+#else
+# define LOGV(...) ((void)0)
+#endif
+
+static void free_saved_state(struct android_app* android_app) {
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->savedState != NULL) {
+ free(android_app->savedState);
+ android_app->savedState = NULL;
+ android_app->savedStateSize = 0;
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+int8_t android_app_read_cmd(struct android_app* android_app) {
+ int8_t cmd;
+ if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) {
+ LOGE("No data on command pipe!");
+ return -1;
+ }
+ if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app);
+ return cmd;
+}
+
+static void print_cur_config(struct android_app* android_app) {
+ char lang[2], country[2];
+ AConfiguration_getLanguage(android_app->config, lang);
+ AConfiguration_getCountry(android_app->config, country);
+
+ LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
+ "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
+ "modetype=%d modenight=%d",
+ AConfiguration_getMcc(android_app->config),
+ AConfiguration_getMnc(android_app->config),
+ lang[0], lang[1], country[0], country[1],
+ AConfiguration_getOrientation(android_app->config),
+ AConfiguration_getTouchscreen(android_app->config),
+ AConfiguration_getDensity(android_app->config),
+ AConfiguration_getKeyboard(android_app->config),
+ AConfiguration_getNavigation(android_app->config),
+ AConfiguration_getKeysHidden(android_app->config),
+ AConfiguration_getNavHidden(android_app->config),
+ AConfiguration_getSdkVersion(android_app->config),
+ AConfiguration_getScreenSize(android_app->config),
+ AConfiguration_getScreenLong(android_app->config),
+ AConfiguration_getUiModeType(android_app->config),
+ AConfiguration_getUiModeNight(android_app->config));
+}
+
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
+ switch (cmd) {
+ case APP_CMD_INPUT_CHANGED:
+ LOGV("APP_CMD_INPUT_CHANGED");
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->inputQueue != NULL) {
+ AInputQueue_detachLooper(android_app->inputQueue);
+ }
+ android_app->inputQueue = android_app->pendingInputQueue;
+ if (android_app->inputQueue != NULL) {
+ LOGV("Attaching input queue to looper");
+ AInputQueue_attachLooper(android_app->inputQueue,
+ android_app->looper, LOOPER_ID_INPUT, NULL,
+ &android_app->inputPollSource);
+ }
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_INIT_WINDOW:
+ LOGV("APP_CMD_INIT_WINDOW");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->window = android_app->pendingWindow;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_TERM_WINDOW:
+ LOGV("APP_CMD_TERM_WINDOW");
+ pthread_cond_broadcast(&android_app->cond);
+ break;
+
+ case APP_CMD_RESUME:
+ case APP_CMD_START:
+ case APP_CMD_PAUSE:
+ case APP_CMD_STOP:
+ LOGV("activityState=%d", cmd);
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->activityState = cmd;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_CONFIG_CHANGED:
+ LOGV("APP_CMD_CONFIG_CHANGED");
+ AConfiguration_fromAssetManager(android_app->config,
+ android_app->activity->assetManager);
+ print_cur_config(android_app);
+ break;
+
+ case APP_CMD_DESTROY:
+ LOGV("APP_CMD_DESTROY");
+ android_app->destroyRequested = 1;
+ break;
+ }
+}
+
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
+ switch (cmd) {
+ case APP_CMD_TERM_WINDOW:
+ LOGV("APP_CMD_TERM_WINDOW");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->window = NULL;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_SAVE_STATE:
+ LOGV("APP_CMD_SAVE_STATE");
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->stateSaved = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ break;
+
+ case APP_CMD_RESUME:
+ free_saved_state(android_app);
+ break;
+ }
+}
+
+void app_dummy() {
+}
+
+static void android_app_destroy(struct android_app* android_app) {
+ LOGV("android_app_destroy!");
+ free_saved_state(android_app);
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->inputQueue != NULL) {
+ AInputQueue_detachLooper(android_app->inputQueue);
+ }
+ AConfiguration_delete(android_app->config);
+ android_app->destroyed = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+ // Can't touch android_app object after this.
+}
+
+static void process_input(struct android_app* app, struct android_poll_source* source) {
+ AInputEvent* event = NULL;
+ while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
+ LOGV("New input event: type=%d", AInputEvent_getType(event));
+ if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
+ continue;
+ }
+ int32_t handled = 0;
+ if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
+ AInputQueue_finishEvent(app->inputQueue, event, handled);
+ }
+}
+
+static void process_cmd(struct android_app* app, struct android_poll_source* source) {
+ int8_t cmd = android_app_read_cmd(app);
+ android_app_pre_exec_cmd(app, cmd);
+ if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
+ android_app_post_exec_cmd(app, cmd);
+}
+
+static void* android_app_entry(void* param) {
+ struct android_app* android_app = (struct android_app*)param;
+
+ android_app->config = AConfiguration_new();
+ AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
+
+ print_cur_config(android_app);
+
+ android_app->cmdPollSource.id = LOOPER_ID_MAIN;
+ android_app->cmdPollSource.app = android_app;
+ android_app->cmdPollSource.process = process_cmd;
+ android_app->inputPollSource.id = LOOPER_ID_INPUT;
+ android_app->inputPollSource.app = android_app;
+ android_app->inputPollSource.process = process_input;
+
+ ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
+ ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
+ &android_app->cmdPollSource);
+ android_app->looper = looper;
+
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->running = 1;
+ pthread_cond_broadcast(&android_app->cond);
+ pthread_mutex_unlock(&android_app->mutex);
+
+ android_main(android_app);
+
+ android_app_destroy(android_app);
+ return NULL;
+}
+
+// --------------------------------------------------------------------
+// Native activity interaction (called from main thread)
+// --------------------------------------------------------------------
+
+static struct android_app* android_app_create(ANativeActivity* activity,
+ void* savedState, size_t savedStateSize) {
+ struct android_app* android_app = calloc(1, sizeof(struct android_app));
+ android_app->activity = activity;
+
+ pthread_mutex_init(&android_app->mutex, NULL);
+ pthread_cond_init(&android_app->cond, NULL);
+
+ if (savedState != NULL) {
+ android_app->savedState = malloc(savedStateSize);
+ android_app->savedStateSize = savedStateSize;
+ memcpy(android_app->savedState, savedState, savedStateSize);
+ }
+
+ int msgpipe[2];
+ if (pipe(msgpipe)) {
+ LOGE("could not create pipe: %s", strerror(errno));
+ return NULL;
+ }
+ android_app->msgread = msgpipe[0];
+ android_app->msgwrite = msgpipe[1];
+
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+ pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
+
+ // Wait for thread to start.
+ pthread_mutex_lock(&android_app->mutex);
+ while (!android_app->running) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+
+ return android_app;
+}
+
+static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
+ if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
+ LOGE("Failure writing android_app cmd: %s", strerror(errno));
+ }
+}
+
+static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->pendingInputQueue = inputQueue;
+ android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED);
+ while (android_app->inputQueue != android_app->pendingInputQueue) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) {
+ pthread_mutex_lock(&android_app->mutex);
+ if (android_app->pendingWindow != NULL) {
+ android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
+ }
+ android_app->pendingWindow = window;
+ if (window != NULL) {
+ android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
+ }
+ while (android_app->window != android_app->pendingWindow) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app_write_cmd(android_app, cmd);
+ while (android_app->activityState != cmd) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+}
+
+static void android_app_free(struct android_app* android_app) {
+ pthread_mutex_lock(&android_app->mutex);
+ android_app_write_cmd(android_app, APP_CMD_DESTROY);
+ while (!android_app->destroyed) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+ pthread_mutex_unlock(&android_app->mutex);
+
+ close(android_app->msgread);
+ close(android_app->msgwrite);
+ pthread_cond_destroy(&android_app->cond);
+ pthread_mutex_destroy(&android_app->mutex);
+ free(android_app);
+}
+
+static struct android_app* ToApp(ANativeActivity* activity) {
+ return (struct android_app*) activity->instance;
+}
+
+static void onDestroy(ANativeActivity* activity) {
+ LOGV("Destroy: %p", activity);
+ android_app_free(ToApp(activity));
+}
+
+static void onStart(ANativeActivity* activity) {
+ LOGV("Start: %p", activity);
+ android_app_set_activity_state(ToApp(activity), APP_CMD_START);
+}
+
+static void onResume(ANativeActivity* activity) {
+ LOGV("Resume: %p", activity);
+ android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
+}
+
+static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
+ LOGV("SaveInstanceState: %p", activity);
+
+ struct android_app* android_app = ToApp(activity);
+ void* savedState = NULL;
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->stateSaved = 0;
+ android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
+ while (!android_app->stateSaved) {
+ pthread_cond_wait(&android_app->cond, &android_app->mutex);
+ }
+
+ if (android_app->savedState != NULL) {
+ savedState = android_app->savedState;
+ *outLen = android_app->savedStateSize;
+ android_app->savedState = NULL;
+ android_app->savedStateSize = 0;
+ }
+
+ pthread_mutex_unlock(&android_app->mutex);
+
+ return savedState;
+}
+
+static void onPause(ANativeActivity* activity) {
+ LOGV("Pause: %p", activity);
+ android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
+}
+
+static void onStop(ANativeActivity* activity) {
+ LOGV("Stop: %p", activity);
+ android_app_set_activity_state(ToApp(activity), APP_CMD_STOP);
+}
+
+static void onConfigurationChanged(ANativeActivity* activity) {
+ LOGV("ConfigurationChanged: %p", activity);
+ android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED);
+}
+
+static void onContentRectChanged(ANativeActivity* activity, const ARect* r) {
+ LOGV("ContentRectChanged: l=%d,t=%d,r=%d,b=%d", r->left, r->top, r->right, r->bottom);
+ struct android_app* android_app = ToApp(activity);
+ pthread_mutex_lock(&android_app->mutex);
+ android_app->contentRect = *r;
+ pthread_mutex_unlock(&android_app->mutex);
+ android_app_write_cmd(ToApp(activity), APP_CMD_CONTENT_RECT_CHANGED);
+}
+
+static void onLowMemory(ANativeActivity* activity) {
+ LOGV("LowMemory: %p", activity);
+ android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY);
+}
+
+static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
+ LOGV("WindowFocusChanged: %p -- %d", activity, focused);
+ android_app_write_cmd(ToApp(activity), focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
+}
+
+static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowCreated: %p -- %p", activity, window);
+ android_app_set_window(ToApp(activity), window);
+}
+
+static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
+ android_app_set_window(ToApp(activity), NULL);
+}
+
+static void onNativeWindowRedrawNeeded(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window);
+ android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED);
+}
+
+static void onNativeWindowResized(ANativeActivity* activity, ANativeWindow* window) {
+ LOGV("NativeWindowResized: %p -- %p", activity, window);
+ android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
+}
+
+static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
+ LOGV("InputQueueCreated: %p -- %p", activity, queue);
+ android_app_set_input(ToApp(activity), queue);
+}
+
+static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
+ LOGV("InputQueueDestroyed: %p -- %p", activity, queue);
+ android_app_set_input(ToApp(activity), NULL);
+}
+
+JNIEXPORT
+void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState, size_t savedStateSize) {
+ LOGV("Creating: %p", activity);
+
+ activity->callbacks->onConfigurationChanged = onConfigurationChanged;
+ activity->callbacks->onContentRectChanged = onContentRectChanged;
+ activity->callbacks->onDestroy = onDestroy;
+ activity->callbacks->onInputQueueCreated = onInputQueueCreated;
+ activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
+ activity->callbacks->onLowMemory = onLowMemory;
+ activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
+ activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
+ activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
+ activity->callbacks->onNativeWindowResized = onNativeWindowResized;
+ activity->callbacks->onPause = onPause;
+ activity->callbacks->onResume = onResume;
+ activity->callbacks->onSaveInstanceState = onSaveInstanceState;
+ activity->callbacks->onStart = onStart;
+ activity->callbacks->onStop = onStop;
+ activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
+
+ activity->instance = android_app_create(activity, savedState, savedStateSize);
+}
diff --git a/native-app-glue/Sources/AndroidNativeAppGlue/include/android_native_app_glue.h b/native-app-glue/Sources/AndroidNativeAppGlue/include/android_native_app_glue.h
new file mode 100644
index 0000000..116c929
--- /dev/null
+++ b/native-app-glue/Sources/AndroidNativeAppGlue/include/android_native_app_glue.h
@@ -0,0 +1,349 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * 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
+ *
+ * http://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.
+ */
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * The native activity interface provided by
+ * is based on a set of application-provided callbacks that will be called
+ * by the Activity's main thread when certain events occur.
+ *
+ * This means that each one of this callbacks _should_ _not_ block, or they
+ * risk having the system force-close the application. This programming
+ * model is direct, lightweight, but constraining.
+ *
+ * The 'android_native_app_glue' static library is used to provide a different
+ * execution model where the application can implement its own main event
+ * loop in a different thread instead. Here's how it works:
+ *
+ * 1/ The application must provide a function named "android_main()" that
+ * will be called when the activity is created, in a new thread that is
+ * distinct from the activity's main thread.
+ *
+ * 2/ android_main() receives a pointer to a valid "android_app" structure
+ * that contains references to other important objects, e.g. the
+ * ANativeActivity object instance the application is running in.
+ *
+ * 3/ the "android_app" object holds an ALooper instance that already
+ * listens to two important things:
+ *
+ * - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX
+ * declarations below.
+ *
+ * - input events coming from the AInputQueue attached to the activity.
+ *
+ * Each of these correspond to an ALooper identifier returned by
+ * ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT,
+ * respectively.
+ *
+ * Your application can use the same ALooper to listen to additional
+ * file-descriptors. They can either be callback based, or with return
+ * identifiers starting with LOOPER_ID_USER.
+ *
+ * 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event,
+ * the returned data will point to an android_poll_source structure. You
+ * can call the process() function on it, and fill in android_app->onAppCmd
+ * and android_app->onInputEvent to be called for your own processing
+ * of the event.
+ *
+ * Alternatively, you can call the low-level functions to read and process
+ * the data directly... look at the process_cmd() and process_input()
+ * implementations in the glue to see how to do this.
+ *
+ * See the sample named "native-activity" that comes with the NDK with a
+ * full usage example. Also look at the JavaDoc of NativeActivity.
+ */
+
+struct android_app;
+
+/**
+ * Data associated with an ALooper fd that will be returned as the "outData"
+ * when that source has data ready.
+ */
+struct android_poll_source {
+ // The identifier of this source. May be LOOPER_ID_MAIN or
+ // LOOPER_ID_INPUT.
+ int32_t id;
+
+ // The android_app this ident is associated with.
+ struct android_app* app;
+
+ // Function to call to perform the standard processing of data from
+ // this source.
+ void (*process)(struct android_app* app, struct android_poll_source* source);
+};
+
+/**
+ * This is the interface for the standard glue code of a threaded
+ * application. In this model, the application's code is running
+ * in its own thread separate from the main thread of the process.
+ * It is not required that this thread be associated with the Java
+ * VM, although it will need to be in order to make JNI calls any
+ * Java objects.
+ */
+struct android_app {
+ // The application can place a pointer to its own state object
+ // here if it likes.
+ void* userData;
+
+ // Fill this in with the function to process main app commands (APP_CMD_*)
+ void (*onAppCmd)(struct android_app* app, int32_t cmd);
+
+ // Fill this in with the function to process input events. At this point
+ // the event has already been pre-dispatched, and it will be finished upon
+ // return. Return 1 if you have handled the event, 0 for any default
+ // dispatching.
+ int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
+
+ // The ANativeActivity object instance that this app is running in.
+ ANativeActivity* activity;
+
+ // The current configuration the app is running in.
+ AConfiguration* config;
+
+ // This is the last instance's saved state, as provided at creation time.
+ // It is NULL if there was no state. You can use this as you need; the
+ // memory will remain around until you call android_app_exec_cmd() for
+ // APP_CMD_RESUME, at which point it will be freed and savedState set to NULL.
+ // These variables should only be changed when processing a APP_CMD_SAVE_STATE,
+ // at which point they will be initialized to NULL and you can malloc your
+ // state and place the information here. In that case the memory will be
+ // freed for you later.
+ void* savedState;
+ size_t savedStateSize;
+
+ // The ALooper associated with the app's thread.
+ ALooper* looper;
+
+ // When non-NULL, this is the input queue from which the app will
+ // receive user input events.
+ AInputQueue* inputQueue;
+
+ // When non-NULL, this is the window surface that the app can draw in.
+ ANativeWindow* window;
+
+ // Current content rectangle of the window; this is the area where the
+ // window's content should be placed to be seen by the user.
+ ARect contentRect;
+
+ // Current state of the app's activity. May be either APP_CMD_START,
+ // APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below.
+ int activityState;
+
+ // This is non-zero when the application's NativeActivity is being
+ // destroyed and waiting for the app thread to complete.
+ int destroyRequested;
+
+ // -------------------------------------------------
+ // Below are "private" implementation of the glue code.
+
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+
+ int msgread;
+ int msgwrite;
+
+ pthread_t thread;
+
+ struct android_poll_source cmdPollSource;
+ struct android_poll_source inputPollSource;
+
+ int running;
+ int stateSaved;
+ int destroyed;
+ int redrawNeeded;
+ AInputQueue* pendingInputQueue;
+ ANativeWindow* pendingWindow;
+ ARect pendingContentRect;
+};
+
+enum {
+ /**
+ * Looper data ID of commands coming from the app's main thread, which
+ * is returned as an identifier from ALooper_pollOnce(). The data for this
+ * identifier is a pointer to an android_poll_source structure.
+ * These can be retrieved and processed with android_app_read_cmd()
+ * and android_app_exec_cmd().
+ */
+ LOOPER_ID_MAIN = 1,
+
+ /**
+ * Looper data ID of events coming from the AInputQueue of the
+ * application's window, which is returned as an identifier from
+ * ALooper_pollOnce(). The data for this identifier is a pointer to an
+ * android_poll_source structure. These can be read via the inputQueue
+ * object of android_app.
+ */
+ LOOPER_ID_INPUT = 2,
+
+ /**
+ * Start of user-defined ALooper identifiers.
+ */
+ LOOPER_ID_USER = 3,
+};
+
+enum {
+ /**
+ * Command from main thread: the AInputQueue has changed. Upon processing
+ * this command, android_app->inputQueue will be updated to the new queue
+ * (or NULL).
+ */
+ APP_CMD_INPUT_CHANGED,
+
+ /**
+ * Command from main thread: a new ANativeWindow is ready for use. Upon
+ * receiving this command, android_app->window will contain the new window
+ * surface.
+ */
+ APP_CMD_INIT_WINDOW,
+
+ /**
+ * Command from main thread: the existing ANativeWindow needs to be
+ * terminated. Upon receiving this command, android_app->window still
+ * contains the existing window; after calling android_app_exec_cmd
+ * it will be set to NULL.
+ */
+ APP_CMD_TERM_WINDOW,
+
+ /**
+ * Command from main thread: the current ANativeWindow has been resized.
+ * Please redraw with its new size.
+ */
+ APP_CMD_WINDOW_RESIZED,
+
+ /**
+ * Command from main thread: the system needs that the current ANativeWindow
+ * be redrawn. You should redraw the window before handing this to
+ * android_app_exec_cmd() in order to avoid transient drawing glitches.
+ */
+ APP_CMD_WINDOW_REDRAW_NEEDED,
+
+ /**
+ * Command from main thread: the content area of the window has changed,
+ * such as from the soft input window being shown or hidden. You can
+ * find the new content rect in android_app::contentRect.
+ */
+ APP_CMD_CONTENT_RECT_CHANGED,
+
+ /**
+ * Command from main thread: the app's activity window has gained
+ * input focus.
+ */
+ APP_CMD_GAINED_FOCUS,
+
+ /**
+ * Command from main thread: the app's activity window has lost
+ * input focus.
+ */
+ APP_CMD_LOST_FOCUS,
+
+ /**
+ * Command from main thread: the current device configuration has changed.
+ */
+ APP_CMD_CONFIG_CHANGED,
+
+ /**
+ * Command from main thread: the system is running low on memory.
+ * Try to reduce your memory use.
+ */
+ APP_CMD_LOW_MEMORY,
+
+ /**
+ * Command from main thread: the app's activity has been started.
+ */
+ APP_CMD_START,
+
+ /**
+ * Command from main thread: the app's activity has been resumed.
+ */
+ APP_CMD_RESUME,
+
+ /**
+ * Command from main thread: the app should generate a new saved state
+ * for itself, to restore from later if needed. If you have saved state,
+ * allocate it with malloc and place it in android_app.savedState with
+ * the size in android_app.savedStateSize. The will be freed for you
+ * later.
+ */
+ APP_CMD_SAVE_STATE,
+
+ /**
+ * Command from main thread: the app's activity has been paused.
+ */
+ APP_CMD_PAUSE,
+
+ /**
+ * Command from main thread: the app's activity has been stopped.
+ */
+ APP_CMD_STOP,
+
+ /**
+ * Command from main thread: the app's activity is being destroyed,
+ * and waiting for the app thread to clean up and exit before proceeding.
+ */
+ APP_CMD_DESTROY,
+};
+
+/**
+ * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
+ * app command message.
+ */
+int8_t android_app_read_cmd(struct android_app* android_app);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * initial pre-processing of the given command. You can perform your own
+ * actions for the command after calling this function.
+ */
+void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * Call with the command returned by android_app_read_cmd() to do the
+ * final post-processing of the given command. You must have done your own
+ * actions for the command before calling this function.
+ */
+void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
+
+/**
+ * No-op function that used to be used to prevent the linker from stripping app
+ * glue code. No longer necessary, since __attribute__((visibility("default")))
+ * does this for us.
+ */
+__attribute__((
+ deprecated("Calls to app_dummy are no longer necessary. See "
+ "https://github.com/android-ndk/ndk/issues/381."))) void
+app_dummy();
+
+/**
+ * This is the function that application code must implement, representing
+ * the main entry to the app.
+ */
+extern void android_main(struct android_app* app);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/native-app-glue/Sources/AndroidOpenGL/include/module.modulemap b/native-app-glue/Sources/AndroidOpenGL/include/module.modulemap
new file mode 100644
index 0000000..f5f8166
--- /dev/null
+++ b/native-app-glue/Sources/AndroidOpenGL/include/module.modulemap
@@ -0,0 +1,5 @@
+module AndroidOpenGL [system] {
+ header "umbrella.h"
+ link "EGL"
+ link "GLESv1_CM"
+}
\ No newline at end of file
diff --git a/native-app-glue/Sources/AndroidOpenGL/include/umbrella.h b/native-app-glue/Sources/AndroidOpenGL/include/umbrella.h
new file mode 100644
index 0000000..cff1295
--- /dev/null
+++ b/native-app-glue/Sources/AndroidOpenGL/include/umbrella.h
@@ -0,0 +1,3 @@
+#include
+#include
+#include
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..c7050c2
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,26 @@
+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 = "Swift Android Examples"
+include(":hello-swift")
+include(":hello-swift-callback")
+include(":hello-swift-library")
+include(":native-activity")
diff --git a/swift-android.gradle.kts b/swift-android.gradle.kts
new file mode 100644
index 0000000..fc7e2b0
--- /dev/null
+++ b/swift-android.gradle.kts
@@ -0,0 +1,299 @@
+// File: swift-android.gradle.kts
+// Swift build script for Android projects using swiftly
+
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Exec
+import org.gradle.api.file.DuplicatesStrategy
+
+// Configuration class for Swift builds
+data class SwiftConfig(
+ var apiLevel: Int = 29, // Default API level
+ var debugAbiFilters: Set = setOf("arm64-v8a"),
+ var debugExtraBuildFlags: List = emptyList(),
+ var releaseAbiFilters: Set = setOf("arm64-v8a", "armeabi-v7a", "x86_64"),
+ var releaseExtraBuildFlags: List = emptyList(),
+ var swiftlyPath: String? = null, // Optional custom swiftly path
+ var swiftSDKPath: String? = null, // Optional custom Swift SDK path
+ var swiftVersion: String = "6.2.0", // Swift version
+ var androidSdkVersion: String = "6.2-RELEASE-android-0.1" // SDK version
+)
+
+// Architecture definitions
+data class Arch(
+ val androidAbi: String,
+ val triple: String,
+ val swiftArch: String,
+ val swiftTarget: String,
+ val variantName: String
+)
+
+val architectures = mapOf(
+ "arm64" to Arch(
+ androidAbi = "arm64-v8a",
+ triple = "aarch64-linux-android",
+ swiftArch = "aarch64",
+ swiftTarget = "aarch64-unknown-linux-android",
+ variantName = "Arm64"
+ ),
+ "armv7" to Arch(
+ androidAbi = "armeabi-v7a",
+ triple = "arm-linux-androideabi",
+ swiftArch = "armv7",
+ swiftTarget = "armv7-unknown-linux-android",
+ variantName = "Armv7"
+ ),
+ "x86_64" to Arch(
+ androidAbi = "x86_64",
+ triple = "x86_64-linux-android",
+ swiftArch = "x86_64",
+ swiftTarget = "x86_64-unknown-linux-android",
+ variantName = "X86_64"
+ ),
+)
+
+// Create or get existing Swift configuration
+val swiftConfig = (project.extensions.findByName("swiftConfig") as? SwiftConfig)
+ ?: SwiftConfig().also {
+ project.extensions.add("swiftConfig", it)
+ }
+
+// Helper function to get swiftly executable path
+fun getSwiftlyPath(): String {
+ // First check if custom path is provided
+ swiftConfig.swiftlyPath?.let {
+ return it
+ }
+
+ // Try to find swiftly in common locations
+ val homeDir = System.getProperty("user.home")
+ val possiblePaths = listOf(
+ "$homeDir/.swiftly/bin/swiftly",
+ "$homeDir/.local/share/swiftly/bin/swiftly",
+ "$homeDir/.local/bin/swiftly",
+ "/usr/local/bin/swiftly",
+ "/opt/homebrew/bin/swiftly",
+ "/root/.local/share/swiftly/bin/swiftly"
+ )
+
+ for (path in possiblePaths) {
+ if (file(path).exists()) {
+ return path
+ }
+ }
+
+ throw GradleException("Switly path not found. Please set swiftConfig.swiftlyPath or install the swiftly.")
+}
+
+fun getSwiftSDKPath(): String {
+ // First check if custom path is provided
+ swiftConfig.swiftSDKPath?.let {
+ return it
+ }
+
+ // Try to find Swift SDK in common locations
+ val homeDir = System.getProperty("user.home")
+ val possiblePaths = listOf(
+ "$homeDir/Library/org.swift.swiftpm/swift-sdks/",
+ "$homeDir/.config/swiftpm/swift-sdks/",
+ "$homeDir/.swiftpm/swift-sdks/",
+ "/root/.swiftpm/swift-sdks/"
+ )
+
+ for (path in possiblePaths) {
+ if (file(path).exists()) {
+ return path
+ }
+ }
+
+ throw GradleException("Swift SDK path not found. Please set swiftConfig.swiftSDKPath or install the Swift SDK for Android.")
+}
+
+// Helper function to get Swift resources path
+fun getSwiftResourcesPath(arch: Arch): String {
+ val sdkVersion = swiftConfig.androidSdkVersion
+ return "${getSwiftSDKPath()}/swift-${sdkVersion}.artifactbundle/swift-android/swift-resources/usr/lib/swift_static-${arch.swiftArch}/"
+}
+
+// Function to create Swift build task
+fun createSwiftBuildTask(
+ buildTypeName: String,
+ arch: Arch,
+ isDebug: Boolean
+): TaskProvider {
+ val taskName = "swiftBuild${arch.variantName}${buildTypeName.replaceFirstChar { it.uppercaseChar() }}"
+
+ return tasks.findByName(taskName)?.let {
+ tasks.named(taskName)
+ } ?: tasks.register(taskName) {
+ val swiftlyPath = getSwiftlyPath()
+ val resourcesPath = getSwiftResourcesPath(arch)
+ val swiftVersion = swiftConfig.swiftVersion
+
+ // Build the SDK name based on architecture
+ val sdkName = "${arch.swiftTarget}${swiftConfig.apiLevel}"
+ val defaultArgs = listOf(
+ "run", "+${swiftVersion}", "swift", "build",
+ "--swift-sdk", sdkName,
+ "-Xswiftc", "-static-stdlib",
+ "-Xswiftc", "-resource-dir",
+ "-Xswiftc", resourcesPath
+ )
+ val configurationArgs = listOf("-c", if (isDebug) "debug" else "release")
+ val extraArgs = if (isDebug) swiftConfig.debugExtraBuildFlags else swiftConfig.releaseExtraBuildFlags
+ val arguments = defaultArgs + configurationArgs + extraArgs
+
+ workingDir("src/main/swift")
+ executable(swiftlyPath)
+ args(arguments)
+
+ doFirst {
+ // Check if swiftly exists
+ if (!file(swiftlyPath).exists() && swiftlyPath != "swiftly") {
+ throw GradleException(
+ "swiftly not found at: $swiftlyPath\n" +
+ "Please install swiftly or configure the path in swiftConfig.swiftlyPath"
+ )
+ }
+
+ // Check if resources directory exists
+ if (!file(resourcesPath).exists()) {
+ println("Warning: Swift resources directory not found at: $resourcesPath")
+ println("You may need to install the Swift SDK for Android")
+ }
+
+ println("Building Swift for ${arch.variantName} ${if (isDebug) "Debug" else "Release"}")
+ println("Using swiftly: $swiftlyPath")
+ println("Swift SDK: $sdkName")
+ }
+ }
+}
+
+// Function to create copy task for Swift libraries
+fun createCopySwiftLibrariesTask(
+ buildTypeName: String,
+ arch: Arch,
+ isDebug: Boolean,
+ swiftBuildTask: TaskProvider
+): TaskProvider {
+ val taskName = "copySwift${arch.variantName}${buildTypeName.replaceFirstChar { it.uppercaseChar() }}"
+
+ return tasks.findByName(taskName)?.let {
+ tasks.named(taskName)
+ } ?: tasks.register(taskName) {
+ val swiftPmBuildPath = if (isDebug) {
+ "src/main/swift/.build/${arch.swiftTarget}${swiftConfig.apiLevel}/debug"
+ } else {
+ "src/main/swift/.build/${arch.swiftTarget}${swiftConfig.apiLevel}/release"
+ }
+
+ dependsOn(swiftBuildTask)
+
+ // Copy c++ shared runtime libraries
+ from("${getSwiftSDKPath()}/swift-${swiftConfig.androidSdkVersion}.artifactbundle/swift-android/ndk-sysroot/usr/lib/${arch.triple}") {
+ include("libc++_shared.so")
+ }
+
+ // Copy built libraries
+ from(fileTree(swiftPmBuildPath) {
+ include("*.so", "*.so.*")
+ })
+
+ if (isDebug) {
+ into("src/debug/jniLibs/${arch.androidAbi}")
+ }
+ else {
+ into("src/release/jniLibs/${arch.androidAbi}")
+ }
+
+ filePermissions {
+ unix("0644".toInt(8))
+ }
+ duplicatesStrategy = DuplicatesStrategy.INCLUDE
+ }
+}
+
+// Function to handle each variant
+fun handleVariant(variant: Any) {
+ val variantClass = variant::class.java
+
+ // Get build type and name using reflection
+ val buildTypeMethod = variantClass.getMethod("getBuildType")
+ val buildType = buildTypeMethod.invoke(variant)
+ val buildTypeClass = buildType::class.java
+
+ val isJniDebuggableMethod = buildTypeClass.getMethod("isJniDebuggable")
+ val isDebug = isJniDebuggableMethod.invoke(buildType) as Boolean
+
+ val getNameMethod = variantClass.getMethod("getName")
+ val variantName = getNameMethod.invoke(variant) as String
+
+ val getBuildTypeNameMethod = buildTypeClass.getMethod("getName")
+ val buildTypeName = getBuildTypeNameMethod.invoke(buildType) as String
+
+ // Get ABI filters
+ val abiFilters = if (isDebug) {
+ swiftConfig.debugAbiFilters
+ } else {
+ swiftConfig.releaseAbiFilters
+ }.takeIf { it.isNotEmpty() } ?: try {
+ val getNdkMethod = buildTypeClass.getMethod("getNdk")
+ val ndk = getNdkMethod.invoke(buildType)
+ val getAbiFiltersMethod = ndk::class.java.getMethod("getAbiFilters")
+ @Suppress("UNCHECKED_CAST")
+ getAbiFiltersMethod.invoke(ndk) as? Set ?: emptySet()
+ } catch (e: Exception) {
+ emptySet()
+ }
+
+ // Create tasks for each architecture
+ architectures.values.forEach { arch ->
+ if (abiFilters.isEmpty() || abiFilters.contains(arch.androidAbi)) {
+ val swiftBuildTask = createSwiftBuildTask(buildTypeName, arch, isDebug)
+ val copyTask = createCopySwiftLibrariesTask(buildTypeName, arch, isDebug, swiftBuildTask)
+
+ // Mount to Android build pipeline - try multiple possible task names
+ val capitalizedVariantName = variantName.replaceFirstChar { it.uppercaseChar() }
+ tasks.findByName("merge${capitalizedVariantName}JniLibFolders")?.let { task ->
+ task.dependsOn(copyTask)
+ }
+ }
+ }
+}
+
+// Apply configuration after project evaluation
+project.afterEvaluate {
+ val androidExtension = project.extensions.findByName("android")
+ if (androidExtension != null) {
+ // Try applicationVariants first (for apps)
+ try {
+ val applicationVariantsMethod = androidExtension::class.java.getMethod("getApplicationVariants")
+ val variants = applicationVariantsMethod.invoke(androidExtension)
+ val allMethod = variants::class.java.getMethod("all", groovy.lang.Closure::class.java)
+
+ allMethod.invoke(variants, object : groovy.lang.Closure(this) {
+ fun doCall(variant: Any) {
+ handleVariant(variant)
+ }
+ })
+ } catch (e: NoSuchMethodException) {
+ // No applicationVariants found...
+ }
+
+ // Try libraryVariants (for libraries)
+ try {
+ val libraryVariantsMethod = androidExtension::class.java.getMethod("getLibraryVariants")
+ val variants = libraryVariantsMethod.invoke(androidExtension)
+ val allMethod = variants::class.java.getMethod("all", groovy.lang.Closure::class.java)
+
+ allMethod.invoke(variants, object : groovy.lang.Closure(this) {
+ fun doCall(variant: Any) {
+ handleVariant(variant)
+ }
+ })
+ } catch (e: NoSuchMethodException) {
+ // No libraryVariants found..
+ }
+ } else {
+ throw GradleException("Android extension not found. Make sure to apply this script after the Android plugin.")
+ }
+}
\ No newline at end of file