Skip to content

Commit df4a923

Browse files
authored
Add AndroidContext and dependency on swift-jni (#2)
* Add AndroidContext and dependency on swift-jni * Update AndroidContext * Add docs and example of using an ANativeActivity * Fix conditional import * Add AndroidContext.assetManager accessor for AndroidAssetManager * Switch CI to Linux * Switch CI to Linux * Fix OSLog import for Linux * Fix OSLog import for Linux * Update CI * Import fixes * Import fixes * Package fixes
1 parent 39f2ecb commit df4a923

File tree

14 files changed

+316
-51
lines changed

14 files changed

+316
-51
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ on:
99
schedule:
1010
- cron: '45 2,13 * * *'
1111
jobs:
12-
test:
13-
# emulator fails to launch with: HVF error: HV_UNSUPPORTED
14-
#runs-on: macos-15
15-
runs-on: macos-13
16-
timeout-minutes: 120
12+
linux-android:
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 30
1715
steps:
1816
- uses: actions/checkout@v4
1917
- name: "Test Swift Package Locally"
2018
run: swift test
2119
- name: "Test Swift Package on Android"
2220
uses: skiptools/swift-android-action@v2
21+
22+
macos-ios:
23+
runs-on: macos-latest
24+
timeout-minutes: 30
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: "Test Swift Package Locally"
28+
run: swift test
2329
- name: "Test Swift Package on iOS"
2430
run: xcodebuild test -sdk "iphonesimulator" -destination "platform=iOS Simulator,name=iPhone 15" -scheme "$(xcodebuild -list -json | jq -r '.workspace.schemes[-1]')"
2531

Package.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ let package = Package(
55
name: "swift-android-native",
66
products: [
77
.library(name: "AndroidNative", targets: ["AndroidNative"]),
8+
.library(name: "AndroidContext", targets: ["AndroidContext"]),
89
.library(name: "AndroidAssetManager", targets: ["AndroidAssetManager"]),
910
.library(name: "AndroidLogging", targets: ["AndroidLogging"]),
1011
.library(name: "AndroidLooper", targets: ["AndroidLooper"]),
1112
.library(name: "AndroidChoreographer", targets: ["AndroidLooper"]),
1213
],
1314
dependencies: [
15+
.package(url: "https://source.skip.tools/swift-jni.git", "0.0.0"..<"2.0.0"),
1416
],
1517
targets: [
1618
.target(name: "AndroidNDK", linkerSettings: [
@@ -28,6 +30,7 @@ let package = Package(
2830
"AndroidSystem",
2931
]),
3032
.target(name: "AndroidAssetManager", dependencies: [
33+
.product(name: "SwiftJNI", package: "swift-jni"),
3134
.target(name: "AndroidNDK", condition: .when(platforms: [.android])),
3235
]),
3336
.testTarget(name: "AndroidAssetManagerTests", dependencies: [
@@ -39,6 +42,13 @@ let package = Package(
3942
.testTarget(name: "AndroidLoggingTests", dependencies: [
4043
"AndroidLogging",
4144
]),
45+
.target(name: "AndroidContext", dependencies: [
46+
"AndroidAssetManager",
47+
.target(name: "AndroidNDK", condition: .when(platforms: [.android])),
48+
]),
49+
.testTarget(name: "AndroidContextTests", dependencies: [
50+
"AndroidContext",
51+
]),
4252
.target(name: "AndroidLooper", dependencies: [
4353
"AndroidSystem",
4454
"AndroidLogging",
@@ -55,7 +65,7 @@ let package = Package(
5565
"AndroidChoreographer",
5666
]),
5767
.target(name: "AndroidNative", dependencies: [
58-
"AndroidAssetManager",
68+
"AndroidContext",
5969
"AndroidLogging",
6070
"AndroidLooper",
6171
"AndroidChoreographer",

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,62 @@ function.
8686
- `OSLogMessage` is simply a typealias to `Swift.String`, and does not implement any of the [redaction features](https://developer.apple.com/documentation/os/logging/generating_log_messages_from_your_code#3665948) of the Darwin version.
8787

8888

89+
# AndroidContext
90+
91+
This module provides a minimal wrapper for [android.content.Context](https://developer.android.com/reference/android/content/Context)
92+
that uses [SwiftJNI](https://github.com/skiptools/swift-jni) to bridge into the global application context.
93+
94+
## Installation
95+
96+
Add the `AndroidContext` module as a conditional dependency for any targets that need it:
97+
98+
```swift
99+
.target(name: "MyTarget", dependencies: [
100+
.product(name: "AndroidContext", package: "swift-android-native", condition: .when(platforms: [.android]))
101+
])
102+
```
103+
104+
## Usage
105+
106+
```swift
107+
let context = try AndroidContext.application
108+
let packageName = try context.getPackageName()
109+
```
110+
111+
## Internals
112+
113+
### Implementation details
114+
115+
By default, the `AndroidContext.application` accessor will try to invoke the JNI method
116+
`android.app.ActivityThread.currentApplication()Landroid/app/Application;` to obtain the
117+
global application context. This can be overridden at app initialization time by setting
118+
the `SWIFT_ANDROID_CONTEXT_FACTORY` environment to a different static accessor, such as:
119+
120+
```swift
121+
// another way to access the global context (deprecated)
122+
setenv("SWIFT_ANDROID_CONTEXT_FACTORY", "android.app.AppGlobals.getInitialApplication()Landroid/app/Application;", 1)
123+
124+
let context = try AndroidContext.application
125+
```
126+
127+
Such setup must be performed before the first time the `AndroidContext.application`
128+
accessor is called, as the result will be cached the first time it is invoked.
129+
130+
Alternatively, if the application bootstrapping code already has access to a
131+
JNI context and `jobject` reference to the application context, it can be
132+
set directly in the static `contextPointer` field. For example,
133+
if your application uses an NDK [ANativeActivity](https://developer.android.com/ndk/reference/struct/a-native-activity)
134+
activity, then the context can be accessed from its reference to the underlying
135+
[android.app.NativeActivity](https://developer.android.com/reference/android/app/NativeActivity)
136+
instance:
137+
138+
```swift
139+
let nativeActivity: ANativeActivity =
140+
AndroidContext.contextPointer = nativeActivity.clazz
141+
let context = try AndroidContext.application // returns the wrapper around the application context
142+
```
143+
144+
89145
# AndroidAssetManager
90146

91147
This module provides an [AssetManager](https://developer.android.com/ndk/reference/group/asset) API for native Swift on Android.

Sources/AndroidAssetManager/AndroidAssetManager.swift

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@
22
#if os(Android)
33
import Android
44
import AndroidNDK
5+
#endif
6+
import SwiftJNI
57
import Foundation
68

79
/// https://developer.android.com/ndk/reference/group/asset
10+
//@available(macOS, unavailable)
11+
@available(iOS, unavailable)
12+
@available(tvOS, unavailable)
13+
@available(watchOS, unavailable)
814
public final class AndroidAssetManager : @unchecked Sendable {
915
let assetManager: OpaquePointer // AAssetManager
1016
typealias AssetHandle = OpaquePointer
1117

1218
/// Create the asset manager from the given JNI environment with a jobject pointer to the Java AssetManager.
13-
public init(env: UnsafeMutablePointer<JNIEnv?>, peer: jobject) {
19+
public init(env: UnsafeMutablePointer<JNIEnv?>, peer: JavaObjectPointer) {
20+
#if !os(Android)
21+
fatalError("only implemented for Android")
22+
#else
1423
self.assetManager = AAssetManager_fromJava(env, peer)
24+
#endif
1525
}
1626

1727
/// List the file names for each asset in the specific directory
1828
public func listAssets(inDirectory directory: String) -> [String]? {
29+
#if !os(Android)
30+
fatalError("only implemented for Android")
31+
#else
1932
guard let assetDir = AAssetManager_openDir(assetManager, directory) else { return nil }
2033
defer { AAssetDir_close(assetDir) }
2134

@@ -24,14 +37,19 @@ public final class AndroidAssetManager : @unchecked Sendable {
2437
assets.append(String(cString: assetName))
2538
}
2639
return assets
40+
#endif
2741
}
2842

2943
/// Opens the asset at the given path with the specified mode, returning nil if the asset was not found.
3044
public func open(from path: String, mode: AssetMode) -> Asset? {
45+
#if !os(Android)
46+
fatalError("only implemented for Android")
47+
#else
3148
guard let handle = AAssetManager_open(self.assetManager, path, mode.assetMode) else {
3249
return nil
3350
}
3451
return Asset(handle: handle)
52+
#endif
3553
}
3654

3755
/// Attempt to read the entire contents from the asset with the given name.
@@ -56,25 +74,41 @@ public final class AndroidAssetManager : @unchecked Sendable {
5674
public func close() {
5775
if closed { return }
5876
closed = true
77+
#if !os(Android)
78+
fatalError("only implemented for Android")
79+
#else
5980
AAsset_close(handle)
81+
#endif
6082
}
6183

6284
/// Returns the total size of the asset data
6385
public var length: Int64 {
6486
assert(!closed, "asset is closed")
87+
#if !os(Android)
88+
fatalError("only implemented for Android")
89+
#else
6590
return AAsset_getLength64(handle)
91+
#endif
6692
}
6793

6894
/// Report the total amount of asset data that can be read from the current position
6995
public var remainingLength: Int64 {
7096
assert(!closed, "asset is closed")
97+
#if !os(Android)
98+
fatalError("only implemented for Android")
99+
#else
71100
return AAsset_getRemainingLength64(handle)
101+
#endif
72102
}
73103

74104
/// Returns whether this asset's internal buffer is allocated in ordinary RAM (i.e. not mmapped).
75105
public var isAllocated: Bool {
76106
assert(!closed, "asset is closed")
107+
#if !os(Android)
108+
fatalError("only implemented for Android")
109+
#else
77110
return AAsset_isAllocated(handle) != 0
111+
#endif
78112
}
79113

80114
/// Attempt to read 'count' bytes of data from the current offset.
@@ -86,7 +120,11 @@ public final class AndroidAssetManager : @unchecked Sendable {
86120
var data = Data(count: len)
87121

88122
let bytesRead: Int32 = try data.withUnsafeMutableBytes { buffer in
123+
#if !os(Android)
124+
fatalError("only implemented for Android")
125+
#else
89126
AAsset_read(handle, buffer, len)
127+
#endif
90128
}
91129

92130
if bytesRead < 0 {
@@ -104,17 +142,25 @@ public final class AndroidAssetManager : @unchecked Sendable {
104142
/// Seek to the specified offset within the asset data.
105143
public func seek(offset: Int64, whence: AssetSeek) -> Int64 {
106144
assert(!closed, "asset is closed")
145+
#if !os(Android)
146+
fatalError("only implemented for Android")
147+
#else
107148
return AAsset_seek64(handle, offset, whence.seekMode)
149+
#endif
108150
}
109151

110152
/// Open a new file descriptor that can be used to read the asset data.
111153
///
112154
/// Returns nil if direct fd access is not possible (for example, if the asset is compressed).
113155
public func openFileDescriptor(offset: inout Int64, outLength: inout Int64) -> Int32? {
114156
assert(!closed, "asset is closed")
157+
#if !os(Android)
158+
fatalError("only implemented for Android")
159+
#else
115160
let fd = AAsset_openFileDescriptor64(handle, &offset, &outLength)
116161
if fd < 0 { return nil }
117162
return fd
163+
#endif
118164
}
119165
}
120166

@@ -125,6 +171,9 @@ public final class AndroidAssetManager : @unchecked Sendable {
125171
case random
126172

127173
var assetMode: Int32 {
174+
#if !os(Android)
175+
fatalError("only implemented for Android")
176+
#else
128177
switch self {
129178
case .buffer:
130179
return Int32(AASSET_MODE_BUFFER)
@@ -133,6 +182,7 @@ public final class AndroidAssetManager : @unchecked Sendable {
133182
case .random:
134183
return Int32(AASSET_MODE_RANDOM)
135184
}
185+
#endif
136186
}
137187
}
138188

@@ -149,15 +199,17 @@ public final class AndroidAssetManager : @unchecked Sendable {
149199
case data
150200

151201
var seekMode: Int32 {
202+
#if !os(Android)
203+
fatalError("only implemented for Android")
204+
#else
152205
switch self {
153206
case .set: return Int32(SEEK_SET)
154207
case .cur: return Int32(SEEK_CUR)
155208
case .end: return Int32(SEEK_END)
156209
case .hole: return Int32(SEEK_HOLE)
157210
case .data: return Int32(SEEK_DATA)
158211
}
212+
#endif
159213
}
160214
}
161215
}
162-
163-
#endif

Sources/AndroidChoreographer/AndroidChoreographer.swift

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
#if os(Android)
33
import Android
44
import AndroidNDK
5+
#endif
56
import AndroidLogging
67
import CoreFoundation
78

8-
let logger = Logger(subsystem: "swift.android.native", category: "AndroidChoreographer")
9+
//let logger = Logger(subsystem: "swift.android.native", category: "AndroidChoreographer")
910

1011
/// https://developer.android.com/ndk/reference/group/choreographer
12+
@available(macOS, unavailable)
13+
@available(iOS, unavailable)
14+
@available(tvOS, unavailable)
15+
@available(watchOS, unavailable)
1116
public final class AndroidChoreographer : @unchecked Sendable {
1217
private let _choreographer: OpaquePointer
1318

@@ -20,7 +25,11 @@ public final class AndroidChoreographer : @unchecked Sendable {
2025
///
2126
/// This must be called on an ALooper thread.
2227
public static var current: AndroidChoreographer {
28+
#if !os(Android)
29+
fatalError("only implemented for Android")
30+
#else
2331
AndroidChoreographer(choreographer: AChoreographer_getInstance())
32+
#endif
2433
}
2534

2635
init(choreographer: OpaquePointer) {
@@ -30,38 +39,17 @@ public final class AndroidChoreographer : @unchecked Sendable {
3039
/// Add a callback to the Choreographer to invoke `_dispatch_main_queue_callback_4CF` on each frame to drain the main queue
3140
public static func setupMainChoreographer() {
3241
if Self.main == nil {
33-
logger.info("setupMainQueue")
42+
//logger.info("setupMainQueue")
3443
Self.main = AndroidChoreographer.current
3544
//enqueueMainChoreographer()
3645
}
3746
}
3847

3948
public func postFrameCallback(_ callback: @convention(c)(Int, UnsafeMutableRawPointer?) -> ()) {
49+
#if !os(Android)
50+
fatalError("only implemented for Android")
51+
#else
4052
AChoreographer_postFrameCallback(_choreographer, callback, nil)
53+
#endif
4154
}
4255
}
43-
44-
// no longer used: we use the AndroidLooper instead, which will be more efficient than trying to drain the main queue on every frame render
45-
46-
//private func enqueueMainChoreographer() {
47-
// AndroidChoreographer.current.postFrameCallback(choreographerCallback)
48-
//}
49-
//
50-
//// C-compatible callback wrapper
51-
//private var choreographerCallback: AChoreographer_frameCallback64 = { _, _ in
52-
// // Drain the main queue
53-
// //_dispatch_main_queue_callback_4CF()
54-
// while CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true) == CFRunLoopRunResult.handledSource {
55-
// // continue handling queued events without a timeout
56-
// }
57-
//
58-
// // AChoreographer_postFrameCallback64 is single-shot, so we need to re-enqueue the callback each frame
59-
// enqueueMainChoreographer()
60-
//}
61-
62-
//// https://github.com/apple-oss-distributions/libdispatch/blob/bd82a60ee6a73b4eca50af028b48643d51aaf1ea/src/queue.c#L8237
63-
//// https://forums.swift.org/t/main-dispatch-queue-in-linux-sdl-app/31708/3
64-
//@_silgen_name("_dispatch_main_queue_callback_4CF")
65-
//func _dispatch_main_queue_callback_4CF()
66-
67-
#endif

0 commit comments

Comments
 (0)