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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## 3.1.1
* Apple: a read, write or subscription the peripheral refuses now reports its ATT error as the matching `UniversalBleErrorCode` (`insufficientEncryption`, `insufficientAuthentication`, `readNotPermitted`, ...), as Android does, instead of `unknownError`. The error code is still in `details`. Codes in `CBErrorDomain` are unchanged: its numbers overlap ATT's with other meanings (#348).
* Bump `bluez` to `^0.9.0`.
* Bump the `pigeon` dev dependency to `^29.0.0`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ func mapErrorCodeToEnum(_ code: String) -> UniversalBleErrorCode {
}
}

/// Maps an ATT error code (`CBATTError.Code` in `CBATTErrorDomain`) to UniversalBleErrorCode,
/// matching Android's `gattStatusToUniversalBleErrorCode` so a refusal reads the same everywhere.
func attErrorToUniversalBleErrorCode(_ code: Int) -> UniversalBleErrorCode {
switch CBATTError.Code(rawValue: code) {
case .invalidHandle: return .invalidHandle
case .readNotPermitted: return .readNotPermitted
case .writeNotPermitted: return .writeNotPermitted
case .invalidPdu: return .invalidPdu
case .insufficientAuthentication: return .insufficientAuthentication
case .requestNotSupported: return .operationNotSupported
case .invalidOffset: return .invalidOffset
case .insufficientAuthorization: return .insufficientAuthorization
case .prepareQueueFull: return .operationInProgress
case .attributeNotFound: return .serviceNotFound
case .attributeNotLong: return .invalidAttributeLength
case .insufficientEncryptionKeySize: return .insufficientKeySize
case .invalidAttributeValueLength: return .invalidAttributeLength
case .unlikelyError: return .failed
case .insufficientEncryption: return .insufficientEncryption
case .unsupportedGroupType: return .operationNotSupported
case .insufficientResources: return .failed
default: return .unknownError
}
}

/// Creates a PigeonError with the error code enum in details
func createFlutterError(
code: UniversalBleErrorCode,
Expand All @@ -143,7 +168,12 @@ extension Error {
let nsError = self as NSError
let errorCode: String = .init(nsError.code)
let errorDescription: String = nsError.localizedDescription
let mappedCode = mapErrorCodeToEnum(errorCode)
// A GATT operation the peripheral refused carries its ATT error code, which no name lookup
// matches. CBErrorDomain reuses the same small numbers for other meanings, so the domain
// decides which table applies.
let mappedCode = nsError.domain == CBATTErrorDomain
? attErrorToUniversalBleErrorCode(nsError.code)
: mapErrorCodeToEnum(errorCode)
return createFlutterError(code: mappedCode, message: errorDescription, details: errorCode)
}

Expand Down
61 changes: 48 additions & 13 deletions example/ios/RunnerTests/RunnerTests.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,61 @@
import Flutter
import UIKit
import CoreBluetooth
import XCTest

@testable import universal_ble

// This demonstrates a simple unit test of the Swift portion of this plugin's implementation.
//
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
// Unit tests of the Swift portion of this plugin. Run from Xcode, or with
// `xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination '<simulator>'`.

class RunnerTests: XCTestCase {

func testGetPlatformVersion() {
let plugin = UniversalBlePlugin()
func testARefusedOperationCarriesItsAttErrorCode() {
let refused = NSError(
domain: CBATTErrorDomain,
code: CBATTError.Code.insufficientEncryption.rawValue,
userInfo: [NSLocalizedDescriptionKey: "Encryption is insufficient."]
)

let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: [])
let error = refused.toFlutterError()

let resultExpectation = expectation(description: "result block must be called.")
plugin.handle(call) { result in
XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion)
resultExpectation.fulfill()
XCTAssertEqual(error.code, UniversalBleErrorCode.insufficientEncryption.rawValue.description)
XCTAssertEqual(error.message, "Encryption is insufficient.")
XCTAssertEqual(error.details as? String, "15")
}

func testEveryAttErrorCodeMapsAsOnAndroid() {
let expected: [(CBATTError.Code, UniversalBleErrorCode)] = [
(.invalidHandle, .invalidHandle),
(.readNotPermitted, .readNotPermitted),
(.writeNotPermitted, .writeNotPermitted),
(.invalidPdu, .invalidPdu),
(.insufficientAuthentication, .insufficientAuthentication),
(.requestNotSupported, .operationNotSupported),
(.invalidOffset, .invalidOffset),
(.insufficientAuthorization, .insufficientAuthorization),
(.prepareQueueFull, .operationInProgress),
(.attributeNotFound, .serviceNotFound),
(.attributeNotLong, .invalidAttributeLength),
(.insufficientEncryptionKeySize, .insufficientKeySize),
(.invalidAttributeValueLength, .invalidAttributeLength),
(.unlikelyError, .failed),
(.insufficientEncryption, .insufficientEncryption),
(.unsupportedGroupType, .operationNotSupported),
(.insufficientResources, .failed),
]

for (attCode, code) in expected {
XCTAssertEqual(attErrorToUniversalBleErrorCode(attCode.rawValue), code, "ATT code \(attCode.rawValue)")
}
waitForExpectations(timeout: 1)
XCTAssertEqual(attErrorToUniversalBleErrorCode(0x80), .unknownError, "an application-defined ATT code")
}

func testACoreBluetoothErrorWithTheSameNumberIsNotReadAsAnAttCode() {
// CBError 15 is "encryption timed out", not ATT's "insufficient encryption".
let timedOut = NSError(domain: CBErrorDomain, code: CBError.Code.encryptionTimedOut.rawValue)

let error = timedOut.toFlutterError()

XCTAssertEqual(error.code, UniversalBleErrorCode.unknownError.rawValue.description)
XCTAssertEqual(error.details as? String, "15")
}
}
Loading