Skip to content

Commit 87898b0

Browse files
committed
Add AndroidAssetManager
1 parent eb40ddd commit 87898b0

File tree

8 files changed

+245
-3
lines changed

8 files changed

+245
-3
lines changed

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ let package = Package(
55
name: "swift-android-native",
66
products: [
77
.library(name: "AndroidNative", targets: ["AndroidNative"]),
8+
.library(name: "AndroidAssetManager", targets: ["AndroidAssetManager"]),
89
.library(name: "AndroidLogging", targets: ["AndroidLogging"]),
910
.library(name: "AndroidLooper", targets: ["AndroidLooper"]),
1011
.library(name: "AndroidChoreographer", targets: ["AndroidLooper"]),
@@ -26,6 +27,12 @@ let package = Package(
2627
.testTarget(name: "AndroidSystemTests", dependencies: [
2728
"AndroidSystem",
2829
]),
30+
.target(name: "AndroidAssetManager", dependencies: [
31+
.target(name: "AndroidNDK", condition: .when(platforms: [.android])),
32+
]),
33+
.testTarget(name: "AndroidAssetManagerTests", dependencies: [
34+
"AndroidAssetManager",
35+
]),
2936
.target(name: "AndroidLogging", dependencies: [
3037
.target(name: "AndroidNDK", condition: .when(platforms: [.android])),
3138
]),
@@ -48,6 +55,7 @@ let package = Package(
4855
"AndroidChoreographer",
4956
]),
5057
.target(name: "AndroidNative", dependencies: [
58+
"AndroidAssetManager",
5159
"AndroidLogging",
5260
"AndroidLooper",
5361
"AndroidChoreographer",

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,58 @@ function.
8585

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

88+
89+
# AndroidAssetManager
90+
91+
This module provides an [AssetManager](https://developer.android.com/ndk/reference/group/asset) API for native Swift on Android.
92+
93+
## Installation
94+
95+
### Swift Package Manager
96+
97+
Add the `AndroidAssetManager` module as a conditional dependency for any targets that need it:
98+
99+
```swift
100+
.target(name: "MyTarget", dependencies: [
101+
.product(name: "AndroidAssetManager", package: "swift-android-native", condition: .when(platforms: [.android]))
102+
])
103+
```
104+
105+
106+
# AndroidChoreographer
107+
108+
This module provides a [Choreographer](https://developer.android.com/ndk/reference/group/choreographer) API for native Swift on Android.
109+
110+
## Installation
111+
112+
### Swift Package Manager
113+
114+
Add the `AndroidChoreographer` module as a conditional dependency for any targets that need it:
115+
116+
```swift
117+
.target(name: "MyTarget", dependencies: [
118+
.product(name: "AndroidChoreographer", package: "swift-android-native", condition: .when(platforms: [.android]))
119+
])
120+
```
121+
122+
123+
# AndroidLooper
124+
125+
This module provides a [Looper](https://developer.android.com/ndk/reference/group/looper) API for native Swift on Android.
126+
127+
## Installation
128+
129+
### Swift Package Manager
130+
131+
Add the `AndroidLooper` module as a conditional dependency for any targets that need it:
132+
133+
```swift
134+
.target(name: "MyTarget", dependencies: [
135+
.product(name: "AndroidLooper", package: "swift-android-native", condition: .when(platforms: [.android]))
136+
])
137+
```
138+
139+
88140
# License
89141

90142
Licensed under the Apache 2.0 license with a runtime library exception,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 Skip
2+
#if os(Android)
3+
import Android
4+
import AndroidNDK
5+
import AndroidLogging
6+
import Foundation
7+
8+
let logger = Logger(subsystem: "swift.android.native", category: "AndroidAssetManager")
9+
10+
/// https://developer.android.com/ndk/reference/group/asset
11+
public final class AndroidAssetManager : @unchecked Sendable {
12+
let assetManager: OpaquePointer // AAssetManager
13+
typealias AssetHandle = OpaquePointer
14+
15+
/// Create the asset manager from the given JNI environment with a jobject pointer to the Java AssetManager.
16+
public init(env: UnsafeMutablePointer<JNIEnv?>, peer: jobject) {
17+
self.assetManager = AAssetManager_fromJava(env, peer)
18+
}
19+
20+
/// List the file names for each asset in the specific directory
21+
public func listAssets(inDirectory directory: String) -> [String]? {
22+
guard let assetDir = AAssetManager_openDir(assetManager, directory) else { return nil }
23+
defer { AAssetDir_close(assetDir) }
24+
25+
var assets: [String] = []
26+
while let assetName = AAssetDir_getNextFileName(assetDir) {
27+
assets.append(String(cString: assetName))
28+
}
29+
return assets
30+
}
31+
32+
/// Opens the asset at the given path with the specified mode, returning nil if the asset was not found.
33+
public func open(from path: String, mode: AssetMode) -> Asset? {
34+
guard let handle = AAssetManager_open(self.assetManager, path, mode.assetMode) else {
35+
return nil
36+
}
37+
return Asset(handle: handle)
38+
}
39+
40+
/// Attempt to read the entire contents from the asset with the given name.
41+
public func load(from path: String) -> Data? {
42+
open(from: path, mode: .buffer)?.read()
43+
}
44+
45+
/// A handle to a given Asset for an AssetManager
46+
public class Asset {
47+
let handle: AssetHandle
48+
var closed = false
49+
50+
init(handle: AssetHandle) {
51+
self.handle = handle
52+
}
53+
54+
deinit {
55+
close()
56+
}
57+
58+
/// Close the asset, freeing all associated resources
59+
public func close() {
60+
if closed { return }
61+
closed = true
62+
AAsset_close(handle)
63+
}
64+
65+
/// Returns the total size of the asset data
66+
public var length: Int64 {
67+
assert(!closed, "asset is closed")
68+
return AAsset_getLength64(handle)
69+
}
70+
71+
/// Report the total amount of asset data that can be read from the current position
72+
public var remainingLength: Int64 {
73+
assert(!closed, "asset is closed")
74+
return AAsset_getRemainingLength64(handle)
75+
}
76+
77+
/// Returns whether this asset's internal buffer is allocated in ordinary RAM (i.e. not mmapped).
78+
public var isAllocated: Bool {
79+
assert(!closed, "asset is closed")
80+
return AAsset_isAllocated(handle) != 0
81+
}
82+
83+
/// Attempt to read 'count' bytes of data from the current offset.
84+
///
85+
/// Returns the number of bytes read, zero on EOF, or nil on error.
86+
public func read(size: Int? = nil) -> Data? {
87+
assert(!closed, "asset is closed")
88+
let len = size ?? Int(self.length)
89+
var data = Data(count: len)
90+
91+
let bytesRead: Int32 = try data.withUnsafeMutableBytes { buffer in
92+
AAsset_read(handle, buffer, len)
93+
}
94+
95+
if bytesRead < 0 {
96+
return nil
97+
}
98+
99+
if Int64(bytesRead) < length {
100+
// Resize if we read less than expected
101+
data = data.prefix(Int(bytesRead))
102+
}
103+
104+
return data
105+
}
106+
107+
/// Seek to the specified offset within the asset data.
108+
public func seek(offset: Int64, whence: AssetSeek) -> Int64 {
109+
assert(!closed, "asset is closed")
110+
return AAsset_seek64(handle, offset, whence.seekMode)
111+
}
112+
113+
/// Open a new file descriptor that can be used to read the asset data.
114+
///
115+
/// Returns nil if direct fd access is not possible (for example, if the asset is compressed).
116+
public func openFileDescriptor(offset: inout Int64, outLength: inout Int64) -> Int32? {
117+
assert(!closed, "asset is closed")
118+
let fd = AAsset_openFileDescriptor64(handle, &offset, &outLength)
119+
if fd < 0 { return nil }
120+
return fd
121+
}
122+
}
123+
124+
/// The mode for opening an asset.
125+
public enum AssetMode {
126+
case buffer
127+
case streaming
128+
case random
129+
130+
var assetMode: Int32 {
131+
switch self {
132+
case .buffer:
133+
return Int32(AASSET_MODE_BUFFER)
134+
case .streaming:
135+
return Int32(AASSET_MODE_STREAMING)
136+
case .random:
137+
return Int32(AASSET_MODE_RANDOM)
138+
}
139+
}
140+
}
141+
142+
public enum AssetSeek {
143+
/// If whence is `SEEK_SET`, the offset is set to offset bytes
144+
case set
145+
/// If whence is `SEEK_CUR`, the offset is set to its current location plus offset bytes
146+
case cur
147+
/// If whence is `SEEK_END`, the offset is set to the size of the file plus offset bytes
148+
case end
149+
/// If whence is `SEEK_HOLE`, the offset is set to the start of the next hole greater than or equal to the supplied offset. The definition of a hole is provided below.
150+
case hole
151+
/// If whence is `SEEK_DATA`, the offset is set to the start of the next non-hole file region greater than or equal to the supplied offset
152+
case data
153+
154+
var seekMode: Int32 {
155+
switch self {
156+
case .set: return Int32(SEEK_SET)
157+
case .cur: return Int32(SEEK_CUR)
158+
case .end: return Int32(SEEK_END)
159+
case .hole: return Int32(SEEK_HOLE)
160+
case .data: return Int32(SEEK_DATA)
161+
}
162+
}
163+
}
164+
}
165+
166+
#endif

Sources/AndroidChoreographer/AndroidChoreographer.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
// Copyright 2025 Skip
12
#if os(Android)
23
import Android
34
import AndroidNDK
45
import AndroidLogging
56
import CoreFoundation
67

7-
let logger = Logger(subsystem: "AndroidChoreographer", category: "AndroidChoreographer")
8+
let logger = Logger(subsystem: "swift.android.native", category: "AndroidChoreographer")
89

10+
/// https://developer.android.com/ndk/reference/group/choreographer
911
public final class AndroidChoreographer : @unchecked Sendable {
1012
private let _choreographer: OpaquePointer
1113

Sources/AndroidLogging/AndroidLogging.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
// Copyright 2025 Skip
12
#if os(Android)
23
import Android
34
import AndroidNDK
45

56
public typealias OSLogMessage = String
67

8+
/// https://developer.android.com/ndk/reference/group/logging
79
public struct Logger : @unchecked Sendable {
810
public let subsystem: String
911
public let category: String

Sources/AndroidLooper/AndroidLooper.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
// Copyright 2025 Skip
22
#if os(Android)
33
import Android
44
import AndroidNDK
@@ -8,9 +8,11 @@ import ConcurrencyRuntimeC
88
import CoreFoundation
99
import Dispatch
1010

11-
let logger = Logger(subsystem: "AndroidLooper", category: "AndroidLooper")
11+
let logger = Logger(subsystem: "swift.android.native", category: "AndroidLooper")
1212

1313
// Much of this is adapted from https://github.com/PADL/AndroidLooper/blob/0f26e1bdb989120f5689d74ea69a0525833ecd52/Sources/AndroidLooper/ALooper.swift
14+
15+
/// https://developer.android.com/ndk/reference/group/looper
1416
public struct AndroidLooper: ~Copyable, @unchecked Sendable {
1517
public enum LooperError: Error {
1618
case addFdFailure
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Copyright 2025 Skip
2+
@_exported import AndroidAssetManager
13
@_exported import AndroidLogging
24
@_exported import AndroidLooper
35
@_exported import AndroidChoreographer
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import XCTest
2+
import AndroidSystem
3+
4+
@available(iOS 14.0, *)
5+
class AndroidAssetManagerTests : XCTestCase {
6+
public func testAssetManager() async throws {
7+
}
8+
}

0 commit comments

Comments
 (0)