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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
name: Swift
on: [push]
jobs:
on:
push:
branches: [master]
workflow_dispatch:
pull_request:

jobs:
android:
name: Android
strategy:
fail-fast: false
matrix:
os: ['ubuntu-24.04', 'macos-15-intel']
swift: ['6.2.3', 'nightly-6.3']
arch: ['aarch64', 'x86_64', 'armv7']
sdk: ['28', '29', '31', '33']
runs-on: macos-15
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: skiptools/actions/setup-skip@v1
with:
install-swift-android-sdk: true
gradle-version: 'none'
swift-version: ${{ matrix.swift }}
swift-android-sdk-version: ${{ matrix.swift }}
- name: "Build Swift Package for Android"
run: |
brew install skiptools/skip/skip || (brew update && brew install skiptools/skip/skip)
skip android sdk install --version ${{ matrix.swift }}
ANDROID_NDK_ROOT="" ANDROID_SDK_VERSION=${{ matrix.sdk }} skip android build --arch ${{ matrix.arch }} --android-api-level ${{ matrix.sdk }}
- name: "Install and launch emulator"
run: |
skip android emulator create --android-api-level ${{ matrix.sdk }}
skip android emulator launch --background
- name: "Test Swift package on emulator"
run: |
skip android test --apk

6 changes: 6 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ var package = Package(
sdkVersionDefine
]
),
.testTarget(
name: "AndroidAppTests",
dependencies: [
"AndroidApp",
]
),
.target(
name: "AndroidContent",
dependencies: [
Expand Down
86 changes: 86 additions & 0 deletions Tests/AndroidAppTests/AndroidAppTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Testing
import CSwiftJavaJNI
import JavaTypes
import SwiftJava
import Android
import AndroidOS
import AndroidApp
import AndroidContent

@Test func testAndroidAppContext() throws {
#expect(AndroidContent.Context.fullJavaClassName == "android.content.Context", "unexpected class name")
#expect(AndroidApp.Application.fullJavaClassName == "android.app.Application", "unexpected class name")

let jvm: JavaVirtualMachine = try #require(JavaVirtualMachine.findCreatedJavaVM())
let env: JNIEnvironment = try jvm.environment()
let app: AndroidApp.Application = try #require(AndroidApp.Application.getApplicationContext(env: env))
let ctx: AndroidContent.Context = AndroidContent.Context(javaHolder: app.javaHolder) // cast android.app.Application to android.content.Context

let name = ctx.getPackageName()
#expect(name == "org.swift.test.swiftandroid", "test harness named by skip android test")

let info: AndroidContent.ApplicationInfo = ctx.getApplicationInfo()
#expect(info.processName == "org.swift.test.swiftandroid")
#expect(info.minSdkVersion == 28)

let looper: AndroidOS.Looper = ctx.getMainLooper()
#expect(looper.isCurrentThread() == false, "tests should not be running on main thread")

let activityManagerOb: JavaObject = ctx.getSystemService(ctx.javaClass.ACTIVITY_SERVICE)
let activityManager: ActivityManager = ActivityManager(javaHolder: activityManagerOb.javaHolder)
#expect(activityManager.javaClass.isUserAMonkey() == false, "aren't we all monkeys, though?")
}

extension AndroidApp.Application {
/// There's no public global way of accessing the Android Context, so we use `android.app.ActivityThread.currentActivityThread().getApplication()`
public static func getApplicationContext(env: JNIEnvironment) -> AndroidApp.Application? {
let jni: JNINativeInterface = env.pointee!.pointee

guard let activityThreadClass: jclass = jni.FindClass(env, "android/app/ActivityThread") else { return nil }

// lookup `currentActivityThread()` and call it
guard let currentActivityThread: jmethodID = jni.GetStaticMethodID(env, activityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;") else { return nil }
guard let activityThread: jobject = jni.CallStaticObjectMethodA(env, activityThreadClass, currentActivityThread, []) else { return nil }

// lookup `getApplication()` and call it
guard let getApplication: jmethodID = jni.GetMethodID(env, activityThreadClass, "getApplication", "()Landroid/app/Application;") else { return nil }
guard let application: jobject = jni.CallObjectMethodA(env, activityThread, getApplication, []) else { return nil }

let contextHolder = JavaObjectHolder(object: application, environment: env)
// this could also be AndroidContent.Context (i.e., android.content.Context)
let ctx = AndroidApp.Application(javaHolder: contextHolder)
return ctx
}
}

extension JavaVirtualMachine {
public static func findCreatedJavaVM() -> JavaVirtualMachine? {
if let sharedJVM = try? JavaVirtualMachine.shared() {
return sharedJVM
}

// manual method: Get the ambient JavaVM by invoking `JNI_GetCreatedJavaVMs` by hook or by crook
typealias JavaVMPtr = UnsafeMutablePointer<JavaVM?>
typealias GetCreatedJavaVMs = @convention(c) (_ pvm: UnsafeMutablePointer<JavaVMPtr?>, _ count: Int32, _ num: UnsafeMutablePointer<Int32>) -> jint

// we need to get the host JVM using JNI_GetCreatedJavaVMs, but it is not exported in jni.h,
// so we need to dlsym it from some library, which has changed over various Android APIs
// libnativehelper.so added in API 31 (https://github.com/android/ndk/issues/1320) to work around "libart.so" no longer being allowed to load
// FIXME: this might not work on API 29 and 30 specifically, since it is after Android started restricting access to the APIs but before they exposed JNI_GetCreatedJavaVMs publicly
for libname in [nil, "libnativehelper.so", "libart.so", "libdvm.so"] {
let lib = dlopen(libname, RTLD_NOW)
guard let getCreatedJavaVMs = dlsym(lib, "JNI_GetCreatedJavaVMs").map({ unsafeBitCast($0, to: (GetCreatedJavaVMs).self) }) else {
continue
}

var runningCount: Int32 = 0
var jvm: JavaVMPtr?
if getCreatedJavaVMs(&jvm, 1, &runningCount) == JNI_OK, let jvm = jvm {
return JavaVirtualMachine(adoptingJVM: jvm)
}
}

return nil
}
}

Loading