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