taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

TagEmulation.swift (7449B)


      1 //
      2 //  TagEmulation.swift
      3 //  HCEtest
      4 //
      5 //  Created by Marc Stibane on 2024-09-01.
      6 //
      7 
      8 import CoreNFC
      9 import os.log
     10 
     11 @available(iOS 17.7, *)
     12 @MainActor
     13 class TagEmulation: ObservableObject {
     14     public static let shared = TagEmulation()
     15     @Published var canUseHCE: Bool = false
     16 
     17     nonisolated let logger = Logger(subsystem: "net.taler.gnu", category: "TagEmulation")
     18     private var cardSession: CardSession? = nil
     19     private var presentmentIntent: NFCPresentmentIntentAssertion? = nil
     20 
     21     init() {
     22         if NFCReaderSession.readingAvailable {
     23             if CardSession.isSupported {
     24                 Task {
     25                     if await CardSession.isEligible {
     26                         logger.info("CardSession is eligible")
     27                         canUseHCE = true
     28                     } else { logger.error("❗️CardSession is not eligible"); canUseHCE = false }
     29                 }
     30             } else { logger.error("❗️CardSession is not supported"); canUseHCE = false }
     31         } else { logger.error("❗️NFCReaderSession is not available"); canUseHCE = false }
     32     }
     33 
     34     func emulateTag(_ emulatedData: String) {
     35         Task() {
     36             let result = await startEmulation(emulatedData)
     37             if result {
     38                 logger.info("Emulation was successful")
     39             } else {
     40                 logger.warning("❗️Emulation was not successful")
     41             }
     42         }
     43     }
     44 
     45     private func startEmulation(_ emulatedData: String) async -> Bool {
     46         // Hold a presentment intent assertion reference to prevent the
     47         // default contactless app from launching. In a real app, monitor
     48         // presentmentIntent.isValid to ensure the assertion remains active.
     49         var result = false
     50 
     51         do {
     52             if presentmentIntent == nil {
     53                 presentmentIntent = try await NFCPresentmentIntentAssertion.acquire()
     54                 /// The presentment intent assertion expires if any of the following occur:
     55                 /// • The presentment intent assertion object deinitializes.
     56                 /// • Your app goes into the background.
     57                 /// • 15 seconds elapse.
     58                 /// After the presentment intent assertion expires, you must wait through a 15-second cool-down period before you can acquire a new instance.
     59                 logger.info("NFCPresentmentIntentAssertion acquired")
     60             } else {
     61                 logger.info("NFCPresentmentIntentAssertion exists")
     62             }
     63             if cardSession == nil {
     64                 cardSession = try await CardSession()
     65                 logger.info("CardSession launched")
     66             } else {
     67                 logger.info("❗️Yikes! CardSession exists")
     68             }
     69             if let cardSession {
     70                 logger.info("cardSession.startEmulation (without waiting for reader)")
     71                 try await cardSession.startEmulation()
     72 
     73                 logger.info("starting eventStream")
     74                 try await eventStream(cardSession, data: emulatedData)
     75                 logger.info("eventStream finished")
     76 
     77                 try? await Task.sleep(nanoseconds: 1_000_000_000 * 3)
     78 
     79                 logger.info("cardSession.invalidate")
     80                 await cardSession.invalidate()
     81                 result = true
     82             }
     83             cardSession = nil
     84         } catch let error {
     85             // Handle failure to acquire NFC presentment intent assertion or card session.
     86             logger.error("❗️NFCPresentmentIntentAssertion not possible: \(error.localizedDescription)")
     87             DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
     88                 self.presentmentIntent = nil /// Release presentment intent assertion.
     89             }
     90             return false
     91         }
     92         return result
     93     }
     94 
     95     func killEmulation() {
     96         Task() {
     97             if let session = cardSession {
     98                 await session.invalidate()
     99                 cardSession = nil
    100             }
    101             logger.info("NFCPresentmentIntentAssertion released")
    102             DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    103                 if self.cardSession == nil {        // don't release if there is a new session
    104                     self.presentmentIntent = nil    // release presentment intent assertion
    105                 }
    106             }
    107         }
    108     }
    109 
    110     private func eventStream(_ mySession: CardSession, data emulatedData: String) async throws {
    111         let apdu = APDU(emulatedData)
    112         // Iterate over events as the card session produces them.
    113         for try await event in mySession.eventStream {
    114 //            if presentmentIntent?.isValid ?? false {
    115                 switch event {
    116                     case .sessionStarted:
    117 #if DEBUG
    118                         let message = "sessionStarted"
    119                         mySession.alertMessage = message
    120                         logger.info("\(message)")
    121 #endif
    122                         break
    123 
    124                     case .readerDetected:
    125                         /// Start card emulation on first detection of an external reader.
    126                         logger.info("readerDetected")
    127 //                        logger.info("cardSession.startEmulation")
    128 //                        try await mySession.startEmulation()
    129 
    130                     case .readerDeselected:
    131                         /// Stop emulation on first notification of RF link loss.
    132                         logger.info("❗️readerDeselected. cardSession.stopEmulation")
    133                         await mySession.stopEmulation(status: .success)
    134                         return
    135 
    136                     case .received(let cardAPDU):
    137                         do {
    138                             /// Call handler to process received input and produce a response.
    139                             let responseAPDU = apdu.processAPDU(cardAPDU.payload)
    140 
    141                             logger.info("sending back data: \(responseAPDU.hexEncodedString())")
    142                             try await cardAPDU.respond(response: responseAPDU)
    143                         } catch {
    144                             /// Handle the error from respond(response:). If the error is
    145                             /// CardSession.Error.transmissionError, then retry by calling
    146                             /// CardSession.APDU.respond(response:) again.
    147                             logger.error("❗️Error while responding: \(error)")
    148                         }
    149 
    150                     case .sessionInvalidated(reason: _):
    151 #if DEBUG
    152                         mySession.alertMessage = "Ending communication with card reader."
    153                         logger.info("❗️cardSession invalidation")
    154 #endif
    155                         /// Handle the reason for session invalidation.
    156                         await mySession.stopEmulation(status: .failure)
    157                         return
    158 
    159                     default:
    160 #if DEBUG
    161                         let message = "Unknown event from card reader."
    162                         mySession.alertMessage = message
    163                         logger.info("❗️\(message)")
    164 #endif
    165                         break
    166                 }
    167 //            } else {
    168 //                logger.error("❗️presentmentIntent is not valid")
    169 //                await mySession.stopEmulation(status: .failure)
    170 //                presentmentIntent = nil /// Release presentment intent assertion.
    171 //                                        // TODO: stop eventStream?
    172 //            }
    173         } // cardSession.eventStream
    174     }
    175 }