From 20433aa0e94c9ddecebcfea724b6d4ad60ae88da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuli=20M=C3=A4=C3=A4tt=C3=A4?= Date: Wed, 27 Nov 2024 16:43:15 +0200 Subject: [PATCH] SDK release 5.9.0 --- .../http/client/RetrofitClient.kt | 2 +- .../java/com/polar/sdk/impl/BDBleApiImpl.kt | 132 ++++++++++++++++-- .../impl/utils/PolarFirmwareUpdateUtils.kt | 21 ++- .../utils/PolarFirmwareUpdateUtilsTest.kt | 42 ++++++ .../sdk/impl/PolarBleApiImpl.swift | 37 +++-- .../impl/utils/PolarFirmwareUpdateUtils.swift | 41 ++++-- .../iOSCommunications/FirmwareUpdateApi.swift | 2 +- .../model/gatt/client/pmd/BlePmdClient.swift | 4 + .../PolarFirmwareUpdateUtilsTest.swift | 32 +++++ 9 files changed, 275 insertions(+), 38 deletions(-) diff --git a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/http/client/RetrofitClient.kt b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/http/client/RetrofitClient.kt index 7910c402..2fe9b479 100644 --- a/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/http/client/RetrofitClient.kt +++ b/sources/Android/android-communications/library/src/main/java/com/polar/androidcommunications/http/client/RetrofitClient.kt @@ -8,7 +8,7 @@ class RetrofitClient { companion object { fun createRetrofitInstance(): Retrofit { return Retrofit.Builder() - .baseUrl("https://firmware-management-app.ds-2012.env.polar.com") + .baseUrl("https://firmware-management.polar.com") .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) .build() diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.kt index 47bb4f37..579b63b2 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.kt +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/BDBleApiImpl.kt @@ -96,6 +96,7 @@ import protocol.PftpResponse.PbPFtpDirectory import protocol.PftpResponse.PbRequestRecordingStatusResult import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File import java.text.ParseException import java.text.SimpleDateFormat import java.util.* @@ -104,6 +105,8 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.regex.Matcher import java.util.regex.Pattern +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream /** @@ -1278,7 +1281,60 @@ class BDBleApiImpl private constructor(context: Context, features: Set + if (count == 0 || entry.path.contains(Regex("""(\D+)(\d+)\.REC"""))) { + val builder = PftpRequest.PbPFtpOperation.newBuilder() + builder.command = PftpRequest.PbPFtpOperation.Command.REMOVE + builder.path = entry.path + return@flatMap client.request(builder.build().toByteArray()) + } else { + Observable.fromIterable(0 until count) + .flatMap { subRecordingIndex -> + val recordingPath = entry.path.replace( + Regex("(\\.REC)$"), + "$subRecordingIndex.REC" + ) + + fileDeletionMap.put(listOf(recordingPath.split("/") + .subList(0, recordingPath.split("/") + .lastIndex - 1))[0].joinToString(separator = "/"), false) + val builder = PftpRequest.PbPFtpOperation.newBuilder() + builder.command = PftpRequest.PbPFtpOperation.Command.REMOVE + builder.path = recordingPath + + client.request(builder.build().toByteArray()).toObservable() + }.ignoreElements().toSingleDefault(count) + } + }.doFinally { + val dir = "/U/0" + var fileList: MutableList = mutableListOf() + listFiles(identifier, dir, + condition = { entry: String -> + entry.matches(Regex("^(\\d{8})(/)")) || + entry.matches(Regex("^(\\d{6})(/)")) || + entry == "R/" || + entry.contains(".REC") && + !entry.contains(".BPB") && + !entry.contains("HIST") + }) + .map { + fileList.add(it) + } + .doFinally { + if (fileList.isEmpty()) { + deleteDataDirectories(identifier).subscribe() + } + } + .doOnError { error -> + BleLogger.e( + TAG, + "Failed to list files from directory $dir from device $identifier. Error: $error" + ) + Completable.error(error) + } + .subscribe() + }.ignoreElement() } else { Completable.error(PolarOperationNotSupported()) } @@ -1522,11 +1578,11 @@ class BDBleApiImpl private constructor(context: Context, features: Set emitter.onError(error) } ) } catch (error: Throwable) { @@ -1912,15 +1968,40 @@ class BDBleApiImpl private constructor(context: Context, features: Set val contentLength = firmwareBytes.contentLength() BleLogger.d(TAG, "FW package for version ${firmwareUpdateResponse.version} downloaded, size: $contentLength bytes") - val unzippedFwPackage = PolarFirmwareUpdateUtils.unzipFirmwarePackage(firmwareBytes.bytes()) + val firmwareFiles = mutableListOf>() + val zipInputStream = ZipInputStream(ByteArrayInputStream(firmwareBytes.bytes())) + var entry: ZipEntry? + val buffer = ByteArray(PolarFirmwareUpdateUtils.BUFFER_SIZE) + while (zipInputStream.nextEntry.also { entry = it } != null) { + val byteArrayOutputStream = ByteArrayOutputStream() + var length: Int + while (zipInputStream.read(buffer).also { length = it } != -1) { + byteArrayOutputStream.write(buffer, 0, length) + } + val fileName = entry!!.name + BleLogger.d(TAG, "Extracted firmware file: $fileName") + firmwareFiles.add(Pair(fileName, byteArrayOutputStream.toByteArray())) + zipInputStream.closeEntry() + } + zipInputStream.close() + + firmwareFiles.sortWith { f1, f2 -> + PolarFirmwareUpdateUtils.FwFileComparator().compare(File(f1.first), File(f2.first)) + } + doFactoryReset(identifier, true) .andThen(Completable.timer(30, TimeUnit.SECONDS)) .andThen(waitDeviceSessionToOpen(identifier, factoryResetMaxWaitTimeSeconds, waitForDeviceDownSeconds = 10L)) .andThen(Completable.timer(5, TimeUnit.SECONDS)) .andThen( - writeFirmwareToDevice(identifier, unzippedFwPackage) - .map { bytesWritten -> - FirmwareUpdateStatus.WritingFwUpdatePackage("Writing firmware update file for version ${firmwareUpdateResponse.version}, bytes written: $bytesWritten/${unzippedFwPackage.size}") + Flowable.fromIterable(firmwareFiles) + .concatMap { firmwareFile -> + writeFirmwareToDevice(identifier, firmwareFile.first, firmwareFile.second) + .map { bytesWritten -> + FirmwareUpdateStatus.WritingFwUpdatePackage( + "Writing firmware update file ${firmwareFile.first}, bytes written: $bytesWritten/${firmwareFile.second.size}" + ) + } } ) } @@ -1934,7 +2015,7 @@ class BDBleApiImpl private constructor(context: Context, features: Set { + private fun writeFirmwareToDevice(deviceId: String, firmwareFilePath: String, firmwareBytes: ByteArray): Flowable { return Flowable.create({ emitter -> try { val session = sessionPsFtpClientReady(deviceId) @@ -2285,10 +2368,10 @@ class BDBleApiImpl private constructor(context: Context, features: Set if (error is PftpResponseError && error.error == PbPFtpError.REBOOTING.number) { @@ -2500,6 +2586,26 @@ class BDBleApiImpl private constructor(context: Context, features: Set { + + return Flowable.fromIterable(fileDeletionMap.asIterable()) + .map { file -> + val dir = listOf(file.key.split("/").subList(0, file.key.split("/").lastIndex))[0].joinToString(separator = "/") + removeSingleFile(identifier, dir) + .doOnError { error -> + BleLogger.e( + TAG, + "Failed to delete data directory $dir from device $identifier. Error: $error" + ) + } + .doOnSuccess { + deleteDayDirectory(identifier, listOf(file.key.split("/").subList(0,file.key.split("/").lastIndex - 1))[0].joinToString(separator = "/")).subscribe() + }.subscribe() + } + + return Flowable.just(Unit) + } + private fun deleteDayDirectory(identifier: String, dir: String): Completable { var fileList: MutableList = mutableListOf() diff --git a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/utils/PolarFirmwareUpdateUtils.kt b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/utils/PolarFirmwareUpdateUtils.kt index a621e490..84a7eb0e 100644 --- a/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/utils/PolarFirmwareUpdateUtils.kt +++ b/sources/Android/android-communications/library/src/sdk/java/com/polar/sdk/impl/utils/PolarFirmwareUpdateUtils.kt @@ -9,15 +9,34 @@ import io.reactivex.rxjava3.core.Single import protocol.PftpRequest import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File import java.util.zip.ZipInputStream internal object PolarFirmwareUpdateUtils { + /** + * Comparator for sorting FW files so that the order doesn't matter as long as + * the SYSUPDAT.IMG file is the last one (since it makes the device boot itself). + */ + class FwFileComparator : Comparator { + companion object { + private const val SYSUPDAT_IMG = "SYSUPDAT.IMG" + } + + override fun compare(f1: File, f2: File): Int { + return when { + f1.name.contains(SYSUPDAT_IMG) -> 1 + f2.name.contains(SYSUPDAT_IMG) -> -1 + else -> 0 + } + } + } + const val FIRMWARE_UPDATE_FILE_PATH = "/SYSUPDAT.IMG" + const val BUFFER_SIZE = 8192 private const val DEVICE_FIRMWARE_INFO_PATH = "/DEVICE.BPB" private const val TAG = "PolarFirmwareUpdateUtils" - private const val BUFFER_SIZE = 8192 fun readDeviceFirmwareInfo(client: BlePsFtpClient, deviceId: String): Single { BleLogger.d(TAG, "readDeviceFirmwareInfo: $deviceId") diff --git a/sources/Android/android-communications/library/src/test/java/com/polar/sdk/api/model/utils/PolarFirmwareUpdateUtilsTest.kt b/sources/Android/android-communications/library/src/test/java/com/polar/sdk/api/model/utils/PolarFirmwareUpdateUtilsTest.kt index be1dd504..80cfa4ca 100644 --- a/sources/Android/android-communications/library/src/test/java/com/polar/sdk/api/model/utils/PolarFirmwareUpdateUtilsTest.kt +++ b/sources/Android/android-communications/library/src/test/java/com/polar/sdk/api/model/utils/PolarFirmwareUpdateUtilsTest.kt @@ -2,6 +2,7 @@ package com.polar.sdk.api.model.utils import com.polar.androidcommunications.api.ble.model.gatt.client.psftp.BlePsFtpClient import com.polar.sdk.impl.utils.PolarFirmwareUpdateUtils +import com.polar.sdk.impl.utils.PolarFirmwareUpdateUtils.FwFileComparator import fi.polar.remote.representation.protobuf.Device import fi.polar.remote.representation.protobuf.Structures.PbVersion import io.mockk.confirmVerified @@ -13,6 +14,7 @@ import org.junit.Assert import org.junit.Test import protocol.PftpRequest import java.io.ByteArrayOutputStream +import java.io.File class PolarFirmwareUpdateUtilsTest { @@ -126,4 +128,44 @@ class PolarFirmwareUpdateUtilsTest { ) ) } + + @Test + fun `FwFileComparator sorts files correctly`() { + // Arrange + val btFile = mockFile("BTUPDAT.BIN") + val sysFile = mockFile("SYSUPDAT.IMG") + val touchFile = mockFile("TCHUPDAT.BIN") + val files = mutableListOf(btFile, sysFile, touchFile) + + // Act + files.sortWith(FwFileComparator()) + + // Assert + Assert.assertEquals(btFile, files[0]) + Assert.assertEquals(touchFile, files[1]) + Assert.assertEquals(sysFile, files[2]) + } + + @Test + fun `FwFileComparator keeps already sorted files`() { + // Arrange + val f1 = mockFile("BTUPDAT.BIN") + val f2 = mockFile("TCHUPDAT.BIN") + val f3 = mockFile("SYSUPDAT.IMG") + val files = mutableListOf(f1, f2, f3) + + // Act + files.sortWith(FwFileComparator()) + + // Assert + Assert.assertEquals(f1, files[0]) + Assert.assertEquals(f2, files[1]) + Assert.assertEquals(f3, files[2]) + } + + private fun mockFile(name: String): File { + val file = mockk() + every { file.name } returns name + return file + } } \ No newline at end of file diff --git a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift index 8987e0fc..dd7fbd0b 100644 --- a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift +++ b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/PolarBleApiImpl.swift @@ -2307,13 +2307,24 @@ extension PolarBleApiImpl: PolarBleApi { return Observable.empty() } - let unzippedContentLength = unzippedFirmwarePackage.count - BleLogger.trace("Firmware package unzipped, size: \(unzippedContentLength) bytes") + let unzippedContentLength = unzippedFirmwarePackage.reduce(0) { $0 + $1.value.count } + BleLogger.trace("Firmware package unzipped, total size: \(unzippedContentLength) bytes") - return self.writeFirmwareToDevice(deviceId: identifier, firmwareBytes: unzippedFirmwarePackage) - .map { bytesWritten -> FirmwareUpdateStatus in - BleLogger.trace("Writing firmware update file, bytes written: \(bytesWritten)/\(unzippedContentLength) bytes") - return FirmwareUpdateStatus.writingFwUpdatePackage(details: "Writing firmware update file, bytes written: \(bytesWritten)/\(unzippedContentLength) bytes") + let sortedFirmwarePackage = unzippedFirmwarePackage.sorted { (file1, file2) -> Bool in + return PolarFirmwareUpdateUtils.FwFileComparator.compare(file1.key, file2.key) == .orderedAscending + } + + return Observable.from(sortedFirmwarePackage) + .flatMap { fileEntry -> Observable in + let fileName = fileEntry.key + let firmwareBytes = fileEntry.value + let filePath = "/\(fileName)" + + return self.writeFirmwareToDevice(deviceId: identifier, firmwareFilePath: filePath, firmwareBytes: firmwareBytes) + .map { bytesWritten -> FirmwareUpdateStatus in + BleLogger.trace("Writing firmware update file \(fileName), bytes written: \(bytesWritten)/\(firmwareBytes.count) bytes") + return FirmwareUpdateStatus.writingFwUpdatePackage(details: "Writing firmware update file \(fileName), bytes written: \(bytesWritten)/\(firmwareBytes.count) bytes") + } } .concat(Observable.just(FirmwareUpdateStatus.finalizingFwUpdate(details: "Finalizing firmware update..."))) } @@ -2324,7 +2335,7 @@ extension PolarBleApiImpl: PolarBleApi { BleLogger.trace("Firmware update is in finalizing stage") BleLogger.trace("Device rebooting") - self.waitDeviceSessionToOpen(deviceId: identifier, timeoutSeconds: Int(rebootMaxWaitTimeSeconds), waitForDeviceDownSeconds: 10) + self.waitDeviceSessionToOpen(deviceId: identifier, timeoutSeconds: Int(factoryResetMaxWaitTimeSeconds), waitForDeviceDownSeconds: 10) .do(onSubscribe: { BleLogger.trace("Waiting for device session to open after reboot with timeout: \(rebootMaxWaitTimeSeconds) seconds") }) @@ -3045,7 +3056,7 @@ extension PolarBleApiImpl: PolarBleApi { ).subscribe() } - private func writeFirmwareToDevice(deviceId: String, firmwareBytes: Data) -> Observable { + private func writeFirmwareToDevice(deviceId: String, firmwareFilePath: String, firmwareBytes: Data) -> Observable { let factoryResetMaxWaitTimeSeconds: TimeInterval = 6 * 60 BleLogger.trace("Write FW to device") return doFactoryReset(deviceId, preservePairingInformation: true) @@ -3060,11 +3071,11 @@ extension PolarBleApiImpl: PolarBleApi { BleLogger.trace("Initialize session") self.sendInitializationAndStartSyncNotifications(client: client) sleep(1) // Some race condition here? - BleLogger.trace("Start \(PolarFirmwareUpdateUtils.FIRMWARE_UPDATE_FILE_PATH) write") + BleLogger.trace("Start \(firmwareFilePath) write") var builder = Protocol_PbPFtpOperation() builder.command = Protocol_PbPFtpOperation.Command.put - builder.path = PolarFirmwareUpdateUtils.FIRMWARE_UPDATE_FILE_PATH + builder.path = firmwareFilePath let proto = try builder.serializedData() return client.write( proto as NSData, @@ -3078,12 +3089,16 @@ extension PolarBleApiImpl: PolarBleApi { .ignoreElements() .asCompletable() .subscribe(onCompleted: { + if firmwareFilePath.contains("SYSUPDAT.IMG") { + BleLogger.trace("Firmware file is SYSUPDAT.IMG, waiting for reboot") + } observer.onCompleted() }, onError: { error in BleLogger.trace("ERROR: \(error.localizedDescription)") if (error.localizedDescription.contains("ResponseError error 1")) { observer.onCompleted() } else { + BleLogger.trace("Error in writeFirmwareToDevice(): \(error.localizedDescription)") observer.onError(error) } }) @@ -3092,7 +3107,7 @@ extension PolarBleApiImpl: PolarBleApi { } return Disposables.create() }) - } + } private func waitDeviceSessionToOpen(deviceId: String, timeoutSeconds: Int, waitForDeviceDownSeconds: Int = 0) -> Completable { diff --git a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/utils/PolarFirmwareUpdateUtils.swift b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/utils/PolarFirmwareUpdateUtils.swift index fcee0903..a4d38eee 100644 --- a/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/utils/PolarFirmwareUpdateUtils.swift +++ b/sources/iOS/ios-communications/Sources/PolarBleSdk/sdk/impl/utils/PolarFirmwareUpdateUtils.swift @@ -7,6 +7,20 @@ import Zip class PolarFirmwareUpdateUtils { static let FIRMWARE_UPDATE_FILE_PATH = "/SYSUPDAT.IMG" static let DEVICE_FIRMWARE_INFO_PATH = "/DEVICE.BPB" + + public class FwFileComparator { + private static let SYSUPDAT_IMG = "SYSUPDAT.IMG" + + static func compare(_ file1: String, _ file2: String) -> ComparisonResult { + if file1.contains(SYSUPDAT_IMG) { + return .orderedDescending + } else if file2.contains(SYSUPDAT_IMG) { + return .orderedAscending + } else { + return .orderedSame + } + } + } static func readDeviceFirmwareInfo(client: BlePsFtpClient, deviceId: String) -> PolarFirmwareVersionInfo? { let semaphore = DispatchSemaphore(value: 0) @@ -75,29 +89,34 @@ class PolarFirmwareUpdateUtils { return available.count > current.count } - static func unzipFirmwarePackage(zippedData: Data) -> Data? { + static func unzipFirmwarePackage(zippedData: Data) -> [String: Data]? { let temporaryDirectory = FileManager.default.temporaryDirectory let zipFilePath = temporaryDirectory.appendingPathComponent(UUID().uuidString + ".zip") do { try zippedData.write(to: zipFilePath) - + let destinationURL = temporaryDirectory.appendingPathComponent(UUID().uuidString) - + try Zip.unzipFile(zipFilePath, destination: destinationURL, overwrite: true, password: nil) - + let contents = try FileManager.default.contentsOfDirectory(at: destinationURL, includingPropertiesForKeys: nil) - guard let fileURL = contents.first else { + guard !contents.isEmpty else { BleLogger.error("unzipFirmwarePackage() error: No files found in the extracted directory") return nil } - - let decompressedData = try Data(contentsOf: fileURL) - + var fileDataDictionary: [String: Data] = [:] + for fileURL in contents { + let fileName = fileURL.lastPathComponent + let decompressedData = try Data(contentsOf: fileURL) + fileDataDictionary[fileName] = decompressedData + BleLogger.trace("Extracted file: \(fileName) - Size: \(decompressedData.count) bytes") + } + try FileManager.default.removeItem(at: zipFilePath) - try FileManager.default.removeItem(at: fileURL) - - return decompressedData + try FileManager.default.removeItem(at: destinationURL) + + return fileDataDictionary } catch { BleLogger.error("Error during unzipFirmwarePackage(): \(error)") return nil diff --git a/sources/iOS/ios-communications/Sources/iOSCommunications/FirmwareUpdateApi.swift b/sources/iOS/ios-communications/Sources/iOSCommunications/FirmwareUpdateApi.swift index 8f33473b..615eb886 100644 --- a/sources/iOS/ios-communications/Sources/iOSCommunications/FirmwareUpdateApi.swift +++ b/sources/iOS/ios-communications/Sources/iOSCommunications/FirmwareUpdateApi.swift @@ -5,7 +5,7 @@ import Alamofire import RxSwift class FirmwareUpdateApi { - let baseURL = "https://firmware-management-app.ds-2012.env.polar.com" + let baseURL = "https://firmware-management.polar.com" func checkFirmwareUpdate(firmwareUpdateRequest: FirmwareUpdateRequest, completion: @escaping (Result) -> Void) { let url = "\(baseURL)/api/v1/firmware-update/check" diff --git a/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/gatt/client/pmd/BlePmdClient.swift b/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/gatt/client/pmd/BlePmdClient.swift index 5aa72316..87acb01c 100644 --- a/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/gatt/client/pmd/BlePmdClient.swift +++ b/sources/iOS/ios-communications/Sources/iOSCommunications/ble/api/model/gatt/client/pmd/BlePmdClient.swift @@ -733,6 +733,10 @@ public class BlePmdClient: BleGattClientBase { try transport.transmitMessage(self, serviceUuid: BlePmdClient.PMD_SERVICE, characteristicUuid: BlePmdClient.PMD_CP, packet: data, withResponse: true) let response = try self.pmdCpResponseQueue.poll(60) let resp = PmdControlPointResponse(response) + + if resp.errorCode != .success { + throw BleGattException.gattAttributeError(errorCode: resp.errorCode.rawValue, errorDescription: resp.errorCode.description) + } var more = resp.more while (more) { let parameters = try self.pmdCpResponseQueue.poll(60) diff --git a/sources/iOS/ios-communications/Tests/PolarBleSdkTests/PolarFirmwareUpdateUtilsTest.swift b/sources/iOS/ios-communications/Tests/PolarBleSdkTests/PolarFirmwareUpdateUtilsTest.swift index 597d9b15..9b9cb792 100644 --- a/sources/iOS/ios-communications/Tests/PolarBleSdkTests/PolarFirmwareUpdateUtilsTest.swift +++ b/sources/iOS/ios-communications/Tests/PolarBleSdkTests/PolarFirmwareUpdateUtilsTest.swift @@ -104,4 +104,36 @@ class PolarFirmwareUpdateUtilsTest: XCTestCase { ) ) } + + func testFwFileComparatorSortsFilesCorrectly() { + // Arrange + let btFile = "BTUPDAT.BIN" + let sysFile = "SYSUPDAT.IMG" + let touchFile = "TCHUPDAT.BIN" + var files = [btFile, sysFile, touchFile] + + // Act + files.sort { PolarFirmwareUpdateUtils.FwFileComparator.compare($0, $1) == .orderedAscending } + + // Assert + XCTAssertEqual(files[0], btFile, "First file should be BTUPDAT.BIN") + XCTAssertEqual(files[1], touchFile, "Second file should be TCHUPDAT.BIN") + XCTAssertEqual(files[2], sysFile, "Last file should be SYSUPDAT.IMG") + } + + func testFwFileComparatorKeepsAlreadySortedFiles() { + // Arrange + let f1 = "BTUPDAT.BIN" + let f2 = "TCHUPDAT.BIN" + let f3 = "SYSUPDAT.IMG" + var files = [f1, f2, f3] + + // Act + files.sort { PolarFirmwareUpdateUtils.FwFileComparator.compare($0, $1) == .orderedAscending } + + // Assert + XCTAssertEqual(files[0], f1, "Files should maintain initial order if already sorted") + XCTAssertEqual(files[1], f2, "Files should maintain initial order if already sorted") + XCTAssertEqual(files[2], f3, "Files should maintain initial order if already sorted") + } }