Skip to content
Merged
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
22 changes: 21 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ var package = Package(
),
.library(
name: "AndroidHardware", targets: ["AndroidHardware"]
),
.library(
name: "AndroidFileManager", targets: ["AndroidFileManager"]
)
],
dependencies: [
Expand Down Expand Up @@ -149,7 +152,8 @@ var package = Package(
"AndroidWebKit",
"AndroidLogging",
"AndroidLooper",
"AndroidHardware"
"AndroidHardware",
"AndroidFileManager"
],
swiftSettings: [
.swiftLanguageMode(.v5),
Expand Down Expand Up @@ -293,6 +297,8 @@ var package = Package(
"AndroidJava",
"AndroidUtil",
"AndroidOS",
"AndroidNDK",
"AndroidFileManager",
],
exclude: ["swift-java.config"],
swiftSettings: [
Expand Down Expand Up @@ -467,6 +473,20 @@ var package = Package(
linkerSettings: [
.linkedLibrary("android", .when(platforms: [.android]))
]
),
.target(
name: "AndroidFileManager",
dependencies: [
"AndroidNDK"
],
swiftSettings: [
.swiftLanguageMode(.v6),
ndkVersionDefine,
sdkVersionDefine
],
linkerSettings: [
.linkedLibrary("android", .when(platforms: [.android]))
]
)
],
swiftLanguageModes: [.v5, .v6]
Expand Down
55 changes: 55 additions & 0 deletions Sources/AndroidContent/AssetManagerNDK.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// AssetManagerNDK.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 2/27/26.
//

#if canImport(Android)
import Android
import AndroidNDK
#endif
import SwiftJava
import CSwiftJavaJNI
import AndroidFileManager

// MARK: - NDK Asset Manager

public extension AssetManager {

/// Equivalent NDK type.
typealias NDK = AndroidFileManager.AssetManager

/// Create a temporary NDK object and perform operations on it.
func withNDK<E, Result>(_ body: (borrowing NDK) throws(E) -> Result) throws(E) -> Result where E: Error {
let ndk = NDK.fromJava(javaThis, environment: javaEnvironment)
return try body(ndk)
}
}

internal extension AndroidFileManager.AssetManager {

/**
* Converts an android.content.res.AssetManager object into an AAssetManager* object.
*
* If the asset manager is null, null is returned.
*
* Available since API level 24.
*
* \param env Java environment. Must not be null.
* \param assetManager android.content.res.AssetManager java object.
*
* \return an AAssetManager object representing the Java AssetManager object. If either parameter
* is null, this will return null.
*/
static func fromJava(_ javaObject: jobject, environment: JNIEnvironment) -> AndroidFileManager.AssetManager {
guard let pointer = AAssetManager_fromJava(environment, javaObject) else {
fatalError("Unable to initialize from Java object")
}
return AndroidFileManager.AssetManager(pointer)
}
}

#if !os(Android)
func AAssetManager_fromJava(_ environment: JNIEnvironment?, _ javaObject: jobject) -> OpaquePointer? { fatalError("stub") }
#endif
12 changes: 12 additions & 0 deletions Sources/AndroidFileManager/AndroidFileManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// AndroidFileManager.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 2/27/26.
//

#if os(Android)
import Android
import AndroidNDK
#endif

124 changes: 124 additions & 0 deletions Sources/AndroidFileManager/Asset.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//
// Asset.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 2/27/26.
//

#if os(Android)
import Android
import AndroidNDK
#endif

/// A handle to an `AAsset`.
///
/// Asset values own their pointer and close it during deinitialization.
public struct Asset: ~Copyable {

internal let pointer: OpaquePointer

internal init(_ pointer: OpaquePointer) {
self.pointer = pointer
}

deinit {
AAsset_close(pointer)
}
}

// MARK: - Properties

public extension Asset {

/// Total uncompressed length of this asset in bytes.
var length: Int64 {
AAsset_getLength64(pointer)
}

/// Remaining unread bytes in this asset.
var remainingLength: Int64 {
AAsset_getRemainingLength64(pointer)
}

/// Whether the asset is backed by a memory allocation.
var isAllocated: Bool {
AAsset_isAllocated(pointer) != 0
}
}

// MARK: - Methods

public extension Asset {

enum SeekOrigin: Int32, Sendable {
case start = 0
case current = 1
case end = 2
}

/// Reads up to `maxCount` bytes from the current cursor position.
func read(maxCount: Int = 4096) throws(AndroidFileManagerError) -> [UInt8] {
guard maxCount > 0 else {
return []
}
var bytes = [UInt8](repeating: 0, count: maxCount)
let count = AAsset_read(pointer, &bytes, maxCount)
guard count >= 0 else {
throw .readAsset(count)
}
return Array(bytes.prefix(Int(count)))
}

/// Reads and returns all remaining bytes.
func readAll(chunkSize: Int = 4096) throws(AndroidFileManagerError) -> [UInt8] {
guard chunkSize > 0 else {
return []
}
var output = [UInt8]()
output.reserveCapacity(Int(max(remainingLength, 0)))
while true {
let chunk = try read(maxCount: chunkSize)
if chunk.isEmpty {
break
}
output.append(contentsOf: chunk)
}
return output
}

/// Seeks the asset cursor and returns the new absolute position.
///
/// - Parameters:
/// - offset: Signed offset.
/// - whence: `SEEK_SET`, `SEEK_CUR`, or `SEEK_END`.
func seek(offset: Int64, whence: SeekOrigin = .start) throws(AndroidFileManagerError) -> Int64 {
let result = AAsset_seek64(pointer, offset, whence.rawValue)
guard result >= 0 else {
throw .seekAsset(result)
}
return result
}

/// Returns a file descriptor and byte range when available.
func openFileDescriptor() -> (fd: Int32, start: Int64, length: Int64)? {
var start: Int64 = 0
var length: Int64 = 0
let fd = AAsset_openFileDescriptor64(pointer, &start, &length)
guard fd >= 0 else {
return nil
}
return (fd, start, length)
}

/// Returns an in-memory buffer, if this asset exposes one.
func withUnsafeBufferPointer<T>(
_ body: (UnsafeRawBufferPointer) throws -> T
) rethrows -> T? {
guard let baseAddress = AAsset_getBuffer(pointer) else {
return nil
}
let count = Int(max(length, 0))
let buffer = UnsafeRawBufferPointer(start: baseAddress, count: count)
return try body(buffer)
}
}
55 changes: 55 additions & 0 deletions Sources/AndroidFileManager/AssetManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// AssetManager.swift
// SwiftAndroid
//
// Created by Alsey Coleman Miller on 2/27/26.
//

#if os(Android)
import Android
import AndroidNDK
#endif

/// Wrapper around Android `AAssetManager`.
public struct AssetManager: @unchecked Sendable {

internal let pointer: OpaquePointer

/// Creates a manager from an existing native pointer.
public init(_ pointer: OpaquePointer) {
self.pointer = pointer
}
}

// MARK: - Methods

public extension AssetManager {

/// Opens an asset by path.
///
/// - Parameters:
/// - path: Relative path under the APK `assets/` directory.
/// - mode: Access hint for Android's asset backend.
func open(_ path: String, mode: AssetMode = .streaming) throws(AndroidFileManagerError) -> Asset {
guard let pointer = path.withCString({
AAssetManager_open(pointer, $0, mode.rawValue)
}) else {
throw .openAsset(path)
}
return Asset(pointer)
}
}

// MARK: - Supporting Types

public extension AssetManager {

/// `AAssetManager_open` mode flags.
enum AssetMode: Int32, Sendable {
case unknown = 0
case random = 1
case streaming = 2
case buffer = 3
}
}

Loading
Loading