From 8765e42152cf37443c04178f5ffe2db679ac1cad Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:14:42 -0500 Subject: [PATCH 01/11] Add `AndroidHardware` target --- Package.swift | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Package.swift b/Package.swift index 1d2ec075..5dc2d734 100644 --- a/Package.swift +++ b/Package.swift @@ -75,6 +75,9 @@ var package = Package( ), .library( name: "AndroidNDK", targets: ["AndroidNDK"] + ), + .library( + name: "AndroidHardware", targets: ["AndroidHardware"] ) ], dependencies: [ @@ -448,6 +451,21 @@ var package = Package( linkerSettings: [ .linkedLibrary("android", .when(platforms: [.android])) ] + ), + .target( + name: "AndroidHardware", + dependencies: [ + "AndroidNDK", + "AndroidLooper" + ], + swiftSettings: [ + .swiftLanguageMode(.v6), + ndkVersionDefine, + sdkVersionDefine + ], + linkerSettings: [ + .linkedLibrary("android", .when(platforms: [.android])) + ] ) ], swiftLanguageModes: [.v5, .v6] From f51b31d666006efcc65d6386d3e8a93af674a620 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:14:57 -0500 Subject: [PATCH 02/11] Add `AndroidSensorError` --- Sources/AndroidHardware/Error.swift | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Sources/AndroidHardware/Error.swift diff --git a/Sources/AndroidHardware/Error.swift b/Sources/AndroidHardware/Error.swift new file mode 100644 index 00000000..71044058 --- /dev/null +++ b/Sources/AndroidHardware/Error.swift @@ -0,0 +1,31 @@ +// +// Error.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +/// Android Sensor Error +public enum AndroidSensorError: Swift.Error { + + /// Unable to get sensor manager instance. + case invalidManager + + /// Unable to create event queue. + case createEventQueue + + /// Unable to enable sensor (result code). + case enableSensor(Int32) + + /// Unable to disable sensor (result code). + case disableSensor(Int32) + + /// Unable to register sensor (result code). + case registerSensor(Int32) + + /// Unable to set event rate (result code). + case setEventRate(Int32) + + /// Error reading sensor events (result code). + case getEvents(Int32) +} From 58fb56a5ec9e85dacf43689b3b25111d22ccaf55 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:15:34 -0500 Subject: [PATCH 03/11] Add `Sensor` --- Sources/AndroidHardware/Sensor.swift | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Sources/AndroidHardware/Sensor.swift diff --git a/Sources/AndroidHardware/Sensor.swift b/Sources/AndroidHardware/Sensor.swift new file mode 100644 index 00000000..684b5660 --- /dev/null +++ b/Sources/AndroidHardware/Sensor.swift @@ -0,0 +1,115 @@ +// +// Sensor.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +#if os(Android) +import Android +import AndroidNDK +#endif + +/// A reference to a hardware sensor. +/// +/// Sensor instances are vended by `SensorManager` and do not have independent +/// ownership — the underlying pointer is valid for the lifetime of the +/// `SensorManager` that created it. +public struct Sensor: @unchecked Sendable { + + internal let pointer: OpaquePointer + + internal init(_ pointer: OpaquePointer) { + self.pointer = pointer + } +} + +// MARK: - Properties + +public extension Sensor { + + /// The name string of the sensor. + var name: String { + guard let cStr = ASensor_getName(pointer) else { return "" } + return String(cString: cStr) + } + + /// The vendor string of the sensor. + var vendor: String { + guard let cStr = ASensor_getVendor(pointer) else { return "" } + return String(cString: cStr) + } + + /// The type of the sensor. + var type: SensorType { + SensorType(rawValue: ASensor_getType(pointer)) + } + + /// The resolution of the sensor in the sensor's unit. + var resolution: Float { + ASensor_getResolution(pointer) + } + + /// The minimum delay in microseconds between two events, or 0 if the sensor + /// reports only when values change. + var minDelay: Int32 { + ASensor_getMinDelay(pointer) + } + + /// The maximum number of events that the hardware FIFO can hold. + var fifoMaxEventCount: Int32 { + ASensor_getFifoMaxEventCount(pointer) + } + + /// The number of events reserved for this sensor in the hardware FIFO. + var fifoReservedEventCount: Int32 { + ASensor_getFifoReservedEventCount(pointer) + } + + /// The type string of the sensor (e.g. `"android.sensor.accelerometer"`). + var stringType: String? { + ASensor_getStringType(pointer).map { String(cString: $0) } + } + + /// The reporting mode of the sensor. + var reportingMode: SensorReportingMode? { + SensorReportingMode(rawValue: ASensor_getReportingMode(pointer)) + } + + /// Whether this sensor is a wake-up sensor. + var isWakeUpSensor: Bool { + ASensor_isWakeUpSensor(pointer) + } + + /// The hardware sensor handle, unique within a device. + var handle: Int32 { + ASensor_getHandle(pointer) + } +} + +// MARK: - Equatable + +extension Sensor: Equatable { + + public static func == (lhs: Sensor, rhs: Sensor) -> Bool { + lhs.pointer == rhs.pointer + } +} + +// MARK: - Hashable + +extension Sensor: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(pointer) + } +} + +// MARK: - CustomStringConvertible + +extension Sensor: CustomStringConvertible { + + public var description: String { + "\(name) (\(vendor))" + } +} From 90f470a4591a47d7d26f9681106160a0f3397260 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:15:42 -0500 Subject: [PATCH 04/11] Add `SensorEvent` --- Sources/AndroidHardware/SensorEvent.swift | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 Sources/AndroidHardware/SensorEvent.swift diff --git a/Sources/AndroidHardware/SensorEvent.swift b/Sources/AndroidHardware/SensorEvent.swift new file mode 100644 index 00000000..a671d084 --- /dev/null +++ b/Sources/AndroidHardware/SensorEvent.swift @@ -0,0 +1,114 @@ +// +// SensorEvent.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +/// A sensor event containing a measurement from a hardware sensor. +public struct SensorEvent: Sendable { + + private let raw: ASensorEvent + + internal init(_ raw: ASensorEvent) { + self.raw = raw + } +} + +// MARK: - Properties + +public extension SensorEvent { + + /// The sensor identifier (matches `Sensor.handle`). + var sensor: Int32 { raw.sensor } + + /// The sensor type. + var type: SensorType { SensorType(rawValue: raw.type) } + + /// The time at which the event occurred, in nanoseconds since boot. + var timestamp: Int64 { raw.timestamp } + + /// Raw floating-point data values (up to 16 floats). + /// + /// The layout depends on `type`. For most sensors the relevant values + /// are in the first 3 elements (x, y, z). + var data: [Float] { + withUnsafeBytes(of: raw._data) { bytes in + Array(bytes.bindMemory(to: Float.self)) + } + } +} + +// MARK: - Convenience Accessors + +public extension SensorEvent { + + /// Acceleration vector in m/s² (x, y, z) — valid for `.accelerometer`, + /// `.linearAcceleration`, and `.gravity` events. + var acceleration: (x: Float, y: Float, z: Float) { + withUnsafeBytes(of: raw._data) { bytes in + let f = bytes.bindMemory(to: Float.self) + return (f[0], f[1], f[2]) + } + } + + /// Rotation rate in rad/s (x, y, z) — valid for `.gyroscope` events. + var angularVelocity: (x: Float, y: Float, z: Float) { + withUnsafeBytes(of: raw._data) { bytes in + let f = bytes.bindMemory(to: Float.self) + return (f[0], f[1], f[2]) + } + } + + /// Magnetic field in μT (x, y, z) — valid for `.magneticField` events. + var magneticField: (x: Float, y: Float, z: Float) { + withUnsafeBytes(of: raw._data) { bytes in + let f = bytes.bindMemory(to: Float.self) + return (f[0], f[1], f[2]) + } + } + + /// Rotation vector (x, y, z, w) — valid for `.rotationVector` and + /// related events. + var rotationVector: (x: Float, y: Float, z: Float, w: Float) { + withUnsafeBytes(of: raw._data) { bytes in + let f = bytes.bindMemory(to: Float.self) + return (f[0], f[1], f[2], f[3]) + } + } + + /// Illuminance in lx — valid for `.light` events. + var light: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } + + /// Distance in cm — valid for `.proximity` events. + var distance: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } + + /// Temperature in °C — valid for `.ambientTemperature` events. + var temperature: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } + + /// Pressure in hPa — valid for `.pressure` events. + var pressure: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } + + /// Relative humidity as a percentage — valid for `.relativeHumidity` events. + var relativeHumidity: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } + + /// Cumulative step count since last reboot — valid for `.stepCounter` events. + var stepCount: UInt64 { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: UInt64.self)[0] } + } + + /// Hinge angle in degrees — valid for `.hingeAngle` events. + var hingeAngle: Float { + withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + } +} From c8766eb304f85a4b47f486628b56329e2665d009 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:16:05 -0500 Subject: [PATCH 05/11] Add `SensorEventQueue` --- .../AndroidHardware/SensorEventQueue.swift | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Sources/AndroidHardware/SensorEventQueue.swift diff --git a/Sources/AndroidHardware/SensorEventQueue.swift b/Sources/AndroidHardware/SensorEventQueue.swift new file mode 100644 index 00000000..e0e90b72 --- /dev/null +++ b/Sources/AndroidHardware/SensorEventQueue.swift @@ -0,0 +1,107 @@ +// +// SensorEventQueue.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +#if os(Android) +import Android +import AndroidNDK +#endif + +/// An event queue for receiving sensor events. +/// +/// Create via `SensorManager.createEventQueue(looper:)`. +/// The queue is automatically destroyed when this value is deinitialized. +public struct SensorEventQueue: ~Copyable { + + internal let queue: OpaquePointer + + /// Retained to call `ASensorManager_destroyEventQueue` on deinit. + internal let manager: OpaquePointer + + internal init(queue: OpaquePointer, manager: OpaquePointer) { + self.queue = queue + self.manager = manager + } + + deinit { + ASensorManager_destroyEventQueue(manager, queue) + } +} + +// MARK: - Methods + +public extension SensorEventQueue { + + /// Enables the sensor at the default sampling rate. + /// + /// Prefer ``register(_:samplingPeriod:maxReportLatency:)`` for more control. + func enable(_ sensor: Sensor) throws(AndroidSensorError) { + let result = ASensorEventQueue_enableSensor(queue, sensor.pointer) + guard result >= 0 else { + throw .enableSensor(result) + } + } + + /// Disables the sensor so it no longer posts events to this queue. + func disable(_ sensor: Sensor) throws(AndroidSensorError) { + let result = ASensorEventQueue_disableSensor(queue, sensor.pointer) + guard result >= 0 else { + throw .disableSensor(result) + } + } + + /// Registers a sensor with this queue, enabling it and setting the desired + /// sampling period and batch latency. + /// + /// - Parameters: + /// - sensor: The sensor to register. + /// - samplingPeriod: Desired sampling period in microseconds. + /// Use `SensorDelay.normal`, `.game`, `.ui`, or `.fastest` as starting points. + /// - maxReportLatency: Maximum delay in microseconds before an event must be + /// reported. Pass `0` to disable batching. + func register( + _ sensor: Sensor, + samplingPeriod: Int32 = SensorDelay.normal, + maxReportLatency: Int64 = 0 + ) throws(AndroidSensorError) { + let result = ASensorEventQueue_registerSensor( + queue, sensor.pointer, samplingPeriod, maxReportLatency + ) + guard result >= 0 else { + throw .registerSensor(result) + } + } + + /// Sets the delivery rate for a sensor that is already registered. + /// + /// - Parameters: + /// - sensor: The registered sensor. + /// - usec: Desired event interval in microseconds. + func setEventRate(for sensor: Sensor, usec: Int32) throws(AndroidSensorError) { + let result = ASensorEventQueue_setEventRate(queue, sensor.pointer, usec) + guard result >= 0 else { + throw .setEventRate(result) + } + } + + /// Returns `true` if there are pending events in the queue. + var hasEvents: Bool { + ASensorEventQueue_hasEvents(queue) > 0 + } + + /// Reads up to `maxCount` pending events from the queue. + /// + /// - Parameter maxCount: Maximum number of events to read per call. + /// - Returns: An array of `SensorEvent` values (may be empty). + func getEvents(maxCount: Int = 16) throws(AndroidSensorError) -> [SensorEvent] { + var buffer = [ASensorEvent](repeating: ASensorEvent(), count: maxCount) + let count = ASensorEventQueue_getEvents(queue, &buffer, maxCount) + guard count >= 0 else { + throw .getEvents(Int32(count)) + } + return buffer.prefix(count).map { SensorEvent($0) } + } +} From 26573acb124c1a229822f35986a73ed941a095ba Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:16:41 -0500 Subject: [PATCH 06/11] Add `SensorManager` --- Sources/AndroidHardware/SensorManager.swift | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Sources/AndroidHardware/SensorManager.swift diff --git a/Sources/AndroidHardware/SensorManager.swift b/Sources/AndroidHardware/SensorManager.swift new file mode 100644 index 00000000..e0bb8d06 --- /dev/null +++ b/Sources/AndroidHardware/SensorManager.swift @@ -0,0 +1,102 @@ +// +// SensorManager.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +#if os(Android) +import Android +import AndroidNDK +#endif +import AndroidLooper + +/// Manages access to the device's hardware sensors. +/// +/// Obtain an instance via ``init(package:)``, then enumerate available sensors +/// or create event queues for receiving data. +public struct SensorManager: @unchecked Sendable { + + internal let pointer: OpaquePointer + + internal init(_ pointer: OpaquePointer) { + self.pointer = pointer + } +} + +// MARK: - Initialization + +public extension SensorManager { + + /// Returns the sensor manager for the given package name. + /// + /// - Parameter package: The package name of the calling application + /// (e.g. `"com.example.myapp"`). + /// - Throws: `AndroidSensorError.invalidManager` if the system could not + /// return a sensor manager for the provided package. + init(package: String) throws(AndroidSensorError) { + guard let pointer = package.withCString({ ASensorManager_getInstanceForPackage($0) }) else { + throw .invalidManager + } + self.init(pointer) + } +} + +// MARK: - Sensors + +public extension SensorManager { + + /// Returns all sensors available on the device. + var sensors: [Sensor] { + var list: UnsafePointer? + let count = ASensorManager_getSensorList(pointer, &list) + guard count > 0, let list else { return [] } + return (0 ..< Int(count)).map { Sensor(list[$0]) } + } + + /// Returns the default sensor of the given type, or `nil` if none exists. + func defaultSensor(type: SensorType) -> Sensor? { + ASensorManager_getDefaultSensor(pointer, type.rawValue).map { Sensor($0) } + } + + /// Returns the default sensor of the given type with optional wake-up support. + /// + /// - Parameters: + /// - type: The sensor type. + /// - wakeUp: Pass `true` to request a wake-up sensor variant. + func defaultSensor(type: SensorType, wakeUp: Bool) -> Sensor? { + ASensorManager_getDefaultSensorEx(pointer, type.rawValue, wakeUp).map { Sensor($0) } + } +} + +// MARK: - Event Queue + +public extension SensorManager { + + /// Creates an event queue associated with the given looper. + /// + /// Events will be delivered to `callback` (if non-nil) on the looper's + /// thread; otherwise your code must call `SensorEventQueue.getEvents()` + /// in response to looper activity signalled on `ident`. + /// + /// - Parameters: + /// - looper: The looper that will receive sensor events. + /// - ident: Identifier returned by `ALooper_pollOnce` when events are + /// available and `callback` is `nil`. Must be ≥ 0 in that case. + /// - callback: Optional C callback invoked on each event batch. + /// - data: User data pointer passed to `callback`. + func createEventQueue( + looper: borrowing Looper, + ident: Int32 = 0, + callback: ALooper_callbackFunc? = nil, + data: UnsafeMutableRawPointer? = nil + ) throws(AndroidSensorError) -> SensorEventQueue { + let queuePointer = looper.withUnsafePointer { looperPtr in + ASensorManager_createEventQueue(pointer, looperPtr, ident, callback, data) + } + guard let queuePointer else { + throw .createEventQueue + } + return SensorEventQueue(queue: queuePointer, manager: pointer) + } +} From 1c70ca40e9527fcec560e975b6d0359a1fed6258 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:16:50 -0500 Subject: [PATCH 07/11] Add `SensorType` --- Sources/AndroidHardware/SensorType.swift | 176 +++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 Sources/AndroidHardware/SensorType.swift diff --git a/Sources/AndroidHardware/SensorType.swift b/Sources/AndroidHardware/SensorType.swift new file mode 100644 index 00000000..49d6bf8d --- /dev/null +++ b/Sources/AndroidHardware/SensorType.swift @@ -0,0 +1,176 @@ +// +// SensorType.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +/// Android sensor type identifier. +public struct SensorType: RawRepresentable, Equatable, Hashable, Sendable { + + public let rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } +} + +// MARK: - Constants + +public extension SensorType { + + /// Measures the acceleration force in m/s² along x, y, z axes including gravity. + static var accelerometer: Self { .init(rawValue: 1) } + + /// Measures the ambient geomagnetic field in μT for all three axes. + static var magneticField: Self { .init(rawValue: 2) } + + /// Measures the orientation of the device in degrees around all three axes. + static var orientation: Self { .init(rawValue: 3) } + + /// Measures the rate of rotation around x, y, z axes in rad/s. + static var gyroscope: Self { .init(rawValue: 4) } + + /// Measures the ambient light level (illuminance) in lx. + static var light: Self { .init(rawValue: 5) } + + /// Measures the ambient air pressure in hPa or mbar. + static var pressure: Self { .init(rawValue: 6) } + + /// Measures the proximity of an object in cm relative to the viewing screen. + static var proximity: Self { .init(rawValue: 8) } + + /// Measures the force of gravity in m/s² along x, y, z axes. + static var gravity: Self { .init(rawValue: 9) } + + /// Measures the acceleration force in m/s² along x, y, z axes, excluding gravity. + static var linearAcceleration: Self { .init(rawValue: 10) } + + /// Measures the orientation of the device as a combination of angle and axis. + static var rotationVector: Self { .init(rawValue: 11) } + + /// Measures the relative ambient humidity in percent. + static var relativeHumidity: Self { .init(rawValue: 12) } + + /// Measures the ambient air temperature in °C. + static var ambientTemperature: Self { .init(rawValue: 13) } + + /// Measures the geomagnetic field for all three axes without hard iron calibration. + static var magneticFieldUncalibrated: Self { .init(rawValue: 14) } + + /// Measures the rotation vector without the geomagnetic field component. + static var gameRotationVector: Self { .init(rawValue: 15) } + + /// Measures the rate of rotation around each axis without drift compensation. + static var gyroscopeUncalibrated: Self { .init(rawValue: 16) } + + /// Triggers an event each time a significant motion is detected. + static var significantMotion: Self { .init(rawValue: 17) } + + /// Triggers an event each time a step is detected. + static var stepDetector: Self { .init(rawValue: 18) } + + /// Reports the cumulative number of steps taken since the last reboot. + static var stepCounter: Self { .init(rawValue: 19) } + + /// Measures the rotation vector based on the geomagnetic field and accelerometer. + static var geomagneticRotationVector: Self { .init(rawValue: 20) } + + /// Measures the heart rate in beats per minute. + static var heartRate: Self { .init(rawValue: 21) } + + /// Measures the pose of the device (rotation + translation) as a 6DoF value. + static var pose6DOF: Self { .init(rawValue: 28) } + + /// Triggers an event when the device is stationary. + static var stationaryDetect: Self { .init(rawValue: 29) } + + /// Triggers an event when the device starts moving. + static var motionDetect: Self { .init(rawValue: 30) } + + /// Triggers an event each heartbeat. + static var heartBeat: Self { .init(rawValue: 31) } + + /// Reports newly connected or disconnected dynamic sensors. + static var dynamicSensorMeta: Self { .init(rawValue: 32) } + + /// Reports additional sensor information. + static var additionalInfo: Self { .init(rawValue: 33) } + + /// Reports when the device transitions between on-body and off-body. + static var lowLatencyOffbodyDetect: Self { .init(rawValue: 34) } + + /// Measures acceleration on all three axes without calibration. + static var accelerometerUncalibrated: Self { .init(rawValue: 35) } + + /// Measures the hinge angle between two integral parts of the device. + static var hingeAngle: Self { .init(rawValue: 36) } + + /// Tracks head orientation and motion. + static var headTracker: Self { .init(rawValue: 37) } + + /// Measures acceleration along a subset of axes. + static var accelerationLimitedAxes: Self { .init(rawValue: 38) } + + /// Measures rotation along a subset of axes. + static var gyroscopeLimitedAxes: Self { .init(rawValue: 39) } + + /// Measures acceleration along a subset of axes without calibration. + static var accelerationLimitedAxesUncalibrated: Self { .init(rawValue: 40) } + + /// Measures rotation along a subset of axes without calibration. + static var gyroscopeLimitedAxesUncalibrated: Self { .init(rawValue: 41) } + + /// Measures the user's heading in degrees relative to magnetic north. + static var heading: Self { .init(rawValue: 42) } +} + +// MARK: - Supporting Types + +/// The accuracy of a sensor measurement. +public enum SensorAccuracy: Int32, Sendable { + + /// The values returned are unreliable. + case minimum = 0 + + /// The sensor has low accuracy. + case low = 1 + + /// The sensor has medium accuracy. + case medium = 2 + + /// The sensor has maximum accuracy. + case high = 3 +} + +/// Describes how a sensor reports data. +public enum SensorReportingMode: Int32, Sendable { + + /// Reports data continuously at a requested rate. + case continuous = 0 + + /// Reports data only when values change. + case onChange = 1 + + /// Reports a single data sample and then disables itself. + case oneShot = 2 + + /// Reports data in a mode specific to the sensor type. + case special = 3 +} + +/// Standard sensor sampling delays. +public enum SensorDelay { + + /// Rate suited for the user interface (200ms). + public static var normal: Int32 { 200_000 } + + /// Rate suited for UI (60ms). + public static var ui: Int32 { 60_000 } + + /// Rate suited for games (20ms). + public static var game: Int32 { 20_000 } + + /// Fastest rate supported by the sensor. + public static var fastest: Int32 { 0 } +} From 5323d980358d67b0e01b45ecd3485ef6ff48bcc3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:17:13 -0500 Subject: [PATCH 08/11] Add hardware syscalls --- Sources/AndroidHardware/Syscalls.swift | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Sources/AndroidHardware/Syscalls.swift diff --git a/Sources/AndroidHardware/Syscalls.swift b/Sources/AndroidHardware/Syscalls.swift new file mode 100644 index 00000000..665ef334 --- /dev/null +++ b/Sources/AndroidHardware/Syscalls.swift @@ -0,0 +1,83 @@ +// +// Syscalls.swift +// SwiftAndroid +// +// Created by Alsey Coleman Miller on 7/6/25. +// + +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#endif + +#if !os(Android) + +func stub() -> Never { + fatalError("Not running on Android") +} + +// MARK: - ASensorEvent + +/** + * A sensor event payload. + * + * This struct must match the C layout of ASensorEvent exactly. + * Total size: 104 bytes on all supported Android ABI targets. + * version(4) + sensor(4) + type(4) + reserved0(4) + + * timestamp(8) + data_union(64) + flags(4) + reserved1(12) + */ +public struct ASensorEvent { + public var version: Int32 // sizeof(struct ASensorEvent) + public var sensor: Int32 // sensor identifier + public var type: Int32 // sensor type + public var reserved0: Int32 + public var timestamp: Int64 // nanoseconds + // Data union: largest member is float[16] = 64 bytes. + // Represented here as eight UInt64 to keep the layout correct + // without needing to replicate the full C union hierarchy. + internal var _data: (UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64, UInt64) + public var flags: UInt32 + internal var _reserved1: (Int32, Int32, Int32) + + public init() { + version = 0; sensor = 0; type = 0; reserved0 = 0; timestamp = 0 + _data = (0, 0, 0, 0, 0, 0, 0, 0) + flags = 0; _reserved1 = (0, 0, 0) + } +} + +// MARK: - ASensorManager + +func ASensorManager_getInstanceForPackage(_ packageName: UnsafePointer?) -> OpaquePointer? { stub() } +func ASensorManager_getSensorList(_ manager: OpaquePointer, _ list: UnsafeMutablePointer?>) -> Int32 { stub() } +func ASensorManager_getDefaultSensor(_ manager: OpaquePointer, _ type: Int32) -> OpaquePointer? { stub() } +func ASensorManager_getDefaultSensorEx(_ manager: OpaquePointer, _ type: Int32, _ wakeUp: Bool) -> OpaquePointer? { stub() } +func ASensorManager_createEventQueue(_ manager: OpaquePointer, _ looper: OpaquePointer, _ ident: Int32, _ callback: ALooper_callbackFunc?, _ data: UnsafeMutableRawPointer?) -> OpaquePointer? { stub() } +func ASensorManager_destroyEventQueue(_ manager: OpaquePointer, _ queue: OpaquePointer) -> Int32 { stub() } + +// MARK: - ASensor + +func ASensor_getName(_ sensor: OpaquePointer) -> UnsafePointer? { stub() } +func ASensor_getVendor(_ sensor: OpaquePointer) -> UnsafePointer? { stub() } +func ASensor_getType(_ sensor: OpaquePointer) -> Int32 { stub() } +func ASensor_getResolution(_ sensor: OpaquePointer) -> Float { stub() } +func ASensor_getMinDelay(_ sensor: OpaquePointer) -> Int32 { stub() } +func ASensor_getFifoMaxEventCount(_ sensor: OpaquePointer) -> Int32 { stub() } +func ASensor_getFifoReservedEventCount(_ sensor: OpaquePointer) -> Int32 { stub() } +func ASensor_getStringType(_ sensor: OpaquePointer) -> UnsafePointer? { stub() } +func ASensor_getReportingMode(_ sensor: OpaquePointer) -> Int32 { stub() } +func ASensor_isWakeUpSensor(_ sensor: OpaquePointer) -> Bool { stub() } +func ASensor_getHandle(_ sensor: OpaquePointer) -> Int32 { stub() } + +// MARK: - ASensorEventQueue + +func ASensorEventQueue_registerSensor(_ queue: OpaquePointer, _ sensor: OpaquePointer, _ samplingPeriodUs: Int32, _ maxBatchReportLatencyUs: Int64) -> Int32 { stub() } +func ASensorEventQueue_enableSensor(_ queue: OpaquePointer, _ sensor: OpaquePointer) -> Int32 { stub() } +func ASensorEventQueue_disableSensor(_ queue: OpaquePointer, _ sensor: OpaquePointer) -> Int32 { stub() } +func ASensorEventQueue_setEventRate(_ queue: OpaquePointer, _ sensor: OpaquePointer, _ usec: Int32) -> Int32 { stub() } +func ASensorEventQueue_hasEvents(_ queue: OpaquePointer) -> Int32 { stub() } +func ASensorEventQueue_getEvents(_ queue: OpaquePointer, _ events: UnsafeMutablePointer, _ count: Int) -> Int { stub() } +func ASensorEventQueue_requestAdditionalInfoEvents(_ queue: OpaquePointer, _ enable: Bool) -> Int32 { stub() } + +#endif From 3f879c167d544462aeac2a25c2e519bed8b2f0cc Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:27:56 -0500 Subject: [PATCH 09/11] Update dependencies --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 5dc2d734..9a526131 100644 --- a/Package.swift +++ b/Package.swift @@ -148,7 +148,8 @@ var package = Package( "AndroidWidget", "AndroidWebKit", "AndroidLogging", - "AndroidLooper" + "AndroidLooper", + "AndroidHardware" ], swiftSettings: [ .swiftLanguageMode(.v5), From dc76aea8758e1632d2aefb63500982e567a5c839 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:31:08 -0500 Subject: [PATCH 10/11] Fixed `SensorEvent` --- Sources/AndroidHardware/SensorEvent.swift | 46 +++++++++++++++------ Sources/AndroidHardware/SensorManager.swift | 7 +++- Sources/AndroidHardware/Syscalls.swift | 4 +- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Sources/AndroidHardware/SensorEvent.swift b/Sources/AndroidHardware/SensorEvent.swift index a671d084..1f57ba21 100644 --- a/Sources/AndroidHardware/SensorEvent.swift +++ b/Sources/AndroidHardware/SensorEvent.swift @@ -5,6 +5,11 @@ // Created by Alsey Coleman Miller on 7/6/25. // +#if canImport(Android) +import Android +import AndroidNDK +#endif + /// A sensor event containing a measurement from a hardware sensor. public struct SensorEvent: Sendable { @@ -13,6 +18,23 @@ public struct SensorEvent: Sendable { internal init(_ raw: ASensorEvent) { self.raw = raw } + + @inline(__always) + internal func withRawDataBytes( + _ body: (UnsafeRawBufferPointer) -> R + ) -> R { + withUnsafeBytes(of: raw) { rawBytes in + // ASensorEvent layout: + // version(4) + sensor(4) + type(4) + reserved0(4) + timestamp(8) + data(64) + let dataOffset = (MemoryLayout.size * 4) + MemoryLayout.size + let dataStart = rawBytes.baseAddress!.advanced(by: dataOffset) + let dataBytes = UnsafeRawBufferPointer( + start: dataStart, + count: MemoryLayout.size * 16 + ) + return body(dataBytes) + } + } } // MARK: - Properties @@ -33,7 +55,7 @@ public extension SensorEvent { /// The layout depends on `type`. For most sensors the relevant values /// are in the first 3 elements (x, y, z). var data: [Float] { - withUnsafeBytes(of: raw._data) { bytes in + withRawDataBytes { bytes in Array(bytes.bindMemory(to: Float.self)) } } @@ -46,7 +68,7 @@ public extension SensorEvent { /// Acceleration vector in m/s² (x, y, z) — valid for `.accelerometer`, /// `.linearAcceleration`, and `.gravity` events. var acceleration: (x: Float, y: Float, z: Float) { - withUnsafeBytes(of: raw._data) { bytes in + withRawDataBytes { bytes in let f = bytes.bindMemory(to: Float.self) return (f[0], f[1], f[2]) } @@ -54,7 +76,7 @@ public extension SensorEvent { /// Rotation rate in rad/s (x, y, z) — valid for `.gyroscope` events. var angularVelocity: (x: Float, y: Float, z: Float) { - withUnsafeBytes(of: raw._data) { bytes in + withRawDataBytes { bytes in let f = bytes.bindMemory(to: Float.self) return (f[0], f[1], f[2]) } @@ -62,7 +84,7 @@ public extension SensorEvent { /// Magnetic field in μT (x, y, z) — valid for `.magneticField` events. var magneticField: (x: Float, y: Float, z: Float) { - withUnsafeBytes(of: raw._data) { bytes in + withRawDataBytes { bytes in let f = bytes.bindMemory(to: Float.self) return (f[0], f[1], f[2]) } @@ -71,7 +93,7 @@ public extension SensorEvent { /// Rotation vector (x, y, z, w) — valid for `.rotationVector` and /// related events. var rotationVector: (x: Float, y: Float, z: Float, w: Float) { - withUnsafeBytes(of: raw._data) { bytes in + withRawDataBytes { bytes in let f = bytes.bindMemory(to: Float.self) return (f[0], f[1], f[2], f[3]) } @@ -79,36 +101,36 @@ public extension SensorEvent { /// Illuminance in lx — valid for `.light` events. var light: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } /// Distance in cm — valid for `.proximity` events. var distance: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } /// Temperature in °C — valid for `.ambientTemperature` events. var temperature: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } /// Pressure in hPa — valid for `.pressure` events. var pressure: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } /// Relative humidity as a percentage — valid for `.relativeHumidity` events. var relativeHumidity: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } /// Cumulative step count since last reboot — valid for `.stepCounter` events. var stepCount: UInt64 { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: UInt64.self)[0] } + withRawDataBytes { $0.bindMemory(to: UInt64.self)[0] } } /// Hinge angle in degrees — valid for `.hingeAngle` events. var hingeAngle: Float { - withUnsafeBytes(of: raw._data) { $0.bindMemory(to: Float.self)[0] } + withRawDataBytes { $0.bindMemory(to: Float.self)[0] } } } diff --git a/Sources/AndroidHardware/SensorManager.swift b/Sources/AndroidHardware/SensorManager.swift index e0bb8d06..a3532cce 100644 --- a/Sources/AndroidHardware/SensorManager.swift +++ b/Sources/AndroidHardware/SensorManager.swift @@ -48,10 +48,13 @@ public extension SensorManager { /// Returns all sensors available on the device. var sensors: [Sensor] { - var list: UnsafePointer? + var list: UnsafePointer? let count = ASensorManager_getSensorList(pointer, &list) guard count > 0, let list else { return [] } - return (0 ..< Int(count)).map { Sensor(list[$0]) } + return (0 ..< Int(count)).compactMap { index in + guard let pointer = list[index] else { return nil } + return Sensor(pointer) + } } /// Returns the default sensor of the given type, or `nil` if none exists. diff --git a/Sources/AndroidHardware/Syscalls.swift b/Sources/AndroidHardware/Syscalls.swift index 665ef334..af01e18e 100644 --- a/Sources/AndroidHardware/Syscalls.swift +++ b/Sources/AndroidHardware/Syscalls.swift @@ -17,6 +17,8 @@ func stub() -> Never { fatalError("Not running on Android") } +typealias ALooper_callbackFunc = @convention(c) (Int32, Int32, UnsafeMutableRawPointer?) -> Int32 + // MARK: - ASensorEvent /** @@ -50,7 +52,7 @@ public struct ASensorEvent { // MARK: - ASensorManager func ASensorManager_getInstanceForPackage(_ packageName: UnsafePointer?) -> OpaquePointer? { stub() } -func ASensorManager_getSensorList(_ manager: OpaquePointer, _ list: UnsafeMutablePointer?>) -> Int32 { stub() } +func ASensorManager_getSensorList(_ manager: OpaquePointer, _ list: UnsafeMutablePointer?>) -> Int32 { stub() } func ASensorManager_getDefaultSensor(_ manager: OpaquePointer, _ type: Int32) -> OpaquePointer? { stub() } func ASensorManager_getDefaultSensorEx(_ manager: OpaquePointer, _ type: Int32, _ wakeUp: Bool) -> OpaquePointer? { stub() } func ASensorManager_createEventQueue(_ manager: OpaquePointer, _ looper: OpaquePointer, _ ident: Int32, _ callback: ALooper_callbackFunc?, _ data: UnsafeMutableRawPointer?) -> OpaquePointer? { stub() } From 0d2bb34853d993915d8c70e84be52bd768c33a97 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 26 Feb 2026 20:51:07 -0500 Subject: [PATCH 11/11] Fixed `Parcel` for Android 29+ --- Sources/AndroidBinder/Error.swift | 13 ++++++++ Sources/AndroidBinder/Parcel.swift | 45 +++++++++++++--------------- Sources/AndroidBinder/Syscalls.swift | 2 +- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/Sources/AndroidBinder/Error.swift b/Sources/AndroidBinder/Error.swift index 5149244a..54861199 100644 --- a/Sources/AndroidBinder/Error.swift +++ b/Sources/AndroidBinder/Error.swift @@ -125,4 +125,17 @@ internal extension binder_status_t { let error = AndroidBinderError(errorCode, file: file, function: function) return .failure(error) } + + func mapError( + as _: T.Type, + file: StaticString = #file, + function: StaticString = #function + ) -> Result { + guard self != STATUS_OK else { + fatalError("mapError(as:) must only be used for non-STATUS_OK results.") + } + let errorCode = AndroidBinderError.ErrorCode(rawValue: self) + let error = AndroidBinderError(errorCode, file: file, function: function) + return .failure(error) + } } diff --git a/Sources/AndroidBinder/Parcel.swift b/Sources/AndroidBinder/Parcel.swift index b582f954..90aa4f92 100644 --- a/Sources/AndroidBinder/Parcel.swift +++ b/Sources/AndroidBinder/Parcel.swift @@ -135,10 +135,8 @@ public extension Parcel { func marshal(start: Int = 0, length: Int? = nil) throws(AndroidBinderError) -> [UInt8] { let len = length ?? (dataSize - start) var buffer = [UInt8](repeating: 0, count: len) - try buffer.withUnsafeMutableBufferPointer { buf in - if let base = buf.baseAddress { - try handle.marshal(into: base, start: start, length: len).get() - } + if let base = buffer.withUnsafeMutableBufferPointer({ $0.baseAddress }) { + try handle.marshal(into: base, start: start, length: len).get() } return buffer } @@ -151,10 +149,8 @@ public extension Parcel { * \param data the bytes to unmarshal. */ func unmarshal(_ data: [UInt8]) throws(AndroidBinderError) { - try data.withUnsafeBufferPointer { buf in - if let base = buf.baseAddress { - try handle.unmarshal(base, length: data.count).get() - } + if let base = data.withUnsafeBufferPointer({ $0.baseAddress }) { + try handle.unmarshal(base, length: data.count).get() } } } @@ -800,16 +796,17 @@ internal extension Parcel.Handle { func readString() -> Result { var ctx = ParcelStringReadContext(buffer: nil) let status = withUnsafeMutablePointer(to: &ctx) { ctxPtr -> binder_status_t in - AParcel_readString(pointer, ctxPtr) { userData, length -> UnsafeMutablePointer? in - guard let userData = userData else { return nil } + AParcel_readString(pointer, ctxPtr) { userData, length, outBuffer -> Bool in + guard let userData = userData else { return false } let buf = UnsafeMutablePointer.allocate(capacity: Int(length) + 1) buf[Int(length)] = 0 userData.assumingMemoryBound(to: ParcelStringReadContext.self).pointee.buffer = buf - return buf + outBuffer?.pointee = buf + return true } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: String.self) } guard let buf = ctx.buffer else { return .success("") } return .success(String(cString: buf)) } @@ -817,7 +814,7 @@ internal extension Parcel.Handle { func readStrongBinder() -> Result { var binderPtr: OpaquePointer? = nil let status = AParcel_readStrongBinder(pointer, &binderPtr) - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: AndroidBinder.self) } guard let ptr = binderPtr else { return .failure(AndroidBinderError(AndroidBinderError.ErrorCode.unexpectedNull)) } @@ -832,7 +829,7 @@ internal extension Parcel.Handle { func readStatusHeader() -> Result { var statusPtr: OpaquePointer? = nil let statusCode = AParcel_readStatusHeader(pointer, &statusPtr) - guard statusCode == STATUS_OK else { return statusCode.mapError() } + guard statusCode == STATUS_OK else { return statusCode.mapError(as: Status.self) } guard let ptr = statusPtr else { return .failure(AndroidBinderError(AndroidBinderError.ErrorCode.unexpectedNull)) } @@ -979,7 +976,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Int8]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1004,7 +1001,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Int32]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1029,7 +1026,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [UInt32]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1054,7 +1051,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Int64]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1079,7 +1076,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [UInt64]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1104,7 +1101,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Float]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1129,7 +1126,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Double]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1154,7 +1151,7 @@ internal extension Parcel.Handle { } } defer { ctx.buffer?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [UInt16]?.self) } if ctx.isNull { return .success(nil) } if let buf = ctx.buffer { return .success(Array(UnsafeBufferPointer(start: buf, count: Int(ctx.count)))) @@ -1184,7 +1181,7 @@ internal extension Parcel.Handle { ) } defer { ctx.elements?.deallocate() } - guard status == STATUS_OK else { return status.mapError() } + guard status == STATUS_OK else { return status.mapError(as: [Bool]?.self) } if ctx.isNull { return .success(nil) } if let elements = ctx.elements { return .success(Array(UnsafeBufferPointer(start: elements, count: Int(ctx.count)))) @@ -1255,4 +1252,4 @@ private struct ParcelUInt16ArrayReadContext { var buffer: UnsafeMutablePointer? var count: Int32 var isNull: Bool -} \ No newline at end of file +} diff --git a/Sources/AndroidBinder/Syscalls.swift b/Sources/AndroidBinder/Syscalls.swift index de0c3ed8..ddae684a 100644 --- a/Sources/AndroidBinder/Syscalls.swift +++ b/Sources/AndroidBinder/Syscalls.swift @@ -81,7 +81,7 @@ func AParcel_readDouble(_ parcel: OpaquePointer, _ outValue: UnsafeMutablePointe func AParcel_readBool(_ parcel: OpaquePointer, _ outValue: UnsafeMutablePointer) -> binder_status_t { stub() } func AParcel_readChar(_ parcel: OpaquePointer, _ outValue: UnsafeMutablePointer) -> binder_status_t { stub() } func AParcel_readByte(_ parcel: OpaquePointer, _ outValue: UnsafeMutablePointer) -> binder_status_t { stub() } -func AParcel_readString(_ parcel: OpaquePointer, _ stringData: UnsafeMutableRawPointer?, _ allocator: (@convention(c) (UnsafeMutableRawPointer?, Int32) -> UnsafeMutablePointer?)?) -> binder_status_t { stub() } +func AParcel_readString(_ parcel: OpaquePointer, _ stringData: UnsafeMutableRawPointer?, _ allocator: (@convention(c) (UnsafeMutableRawPointer?, Int32, UnsafeMutablePointer?>?) -> Bool)?) -> binder_status_t { stub() } func AParcel_readStrongBinder(_ parcel: OpaquePointer, _ outBinder: UnsafeMutablePointer) -> binder_status_t { stub() } func AParcel_readParcelFileDescriptor(_ parcel: OpaquePointer, _ outFd: UnsafeMutablePointer) -> binder_status_t { stub() } func AParcel_readStatusHeader(_ parcel: OpaquePointer, _ outStatus: UnsafeMutablePointer) -> binder_status_t { stub() }