diff --git a/NearDrop/AppDelegate.swift b/NearDrop/AppDelegate.swift index 625e54a..942ab0b 100644 --- a/NearDrop/AppDelegate.swift +++ b/NearDrop/AppDelegate.swift @@ -115,7 +115,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele @objc func openWelcomeScreen() { // Create the welcome screen SwiftUI view let welcomeView = WelcomeScreen { - self.openPlusScreen(continueTransfer: {}) + self.openPlusScreen() } // Create an NSWindow to host the SwiftUI view @@ -142,11 +142,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } - @objc func openPlusScreen(continueTransfer: @escaping () -> Void) { + @objc func openPlusScreen() { + // Create the welcome screen SwiftUI view let plusView = GetPlusView(closeView: { + log("Closing plus screen") self.plusWindow?.close() - continueTransfer() }) // Create an NSWindow to host the SwiftUI view @@ -210,8 +211,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele } - public func continueTransmission(accept: Bool, transferID: String) { - NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept) + public func continueTransmission(accept: Bool, transferID: String, storeInTemp: Bool = false) { + NearbyConnectionManager.shared.submitUserConsent(transferID: transferID, accept: accept, storeInTemp: storeInTemp) if !accept { activeIncomingTransfers.removeValue(forKey: transferID) @@ -284,10 +285,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele private func pressAcceptButton(transferID: String) { - if !isPlusVersion() && transmissionCount() > 0 { - self.openPlusScreen { - self.continueTransmission(accept: self.isPlusVersion(), transferID: transferID) - } + if false || (!isPlusVersion() && transmissionCount() > 0) { + + self.continueTransmission(accept: true, transferID: transferID, storeInTemp: true) + log("Showing plus screen...") + + self.openPlusScreen() } else { continueTransmission(accept: true, transferID: transferID) diff --git a/NearDrop/Base.lproj/Localizable.strings b/NearDrop/Base.lproj/Localizable.strings index 61db555..6b4716f 100644 --- a/NearDrop/Base.lproj/Localizable.strings +++ b/NearDrop/Base.lproj/Localizable.strings @@ -91,7 +91,7 @@ If you still encounter issues, the issues could be caused due to your WiFi route "plusview_title" = "Thanks for using QuickDrop!"; "plusview_description" = "Unlock QuickDrop+ to continue exchanging files without limits. QuickDrop+ is a one-time purchase – no subscription required. All current and future features are included."; "plusview_success_title" = "Thanks for your purchase!"; -"plusview_success_description" = "You can now continue receiving files without restrictions."; +"plusview_success_description" = "You can now continue receiving files without restrictions. Any files you tried to send were already saved on your device."; "plusview_success_button" = "Got it!"; "plusview_unlock_label" = "Support QuickDrop"; diff --git a/NearDrop/IAP/GetPlusView.swift b/NearDrop/IAP/GetPlusView.swift index 2034e01..1258ae4 100644 --- a/NearDrop/IAP/GetPlusView.swift +++ b/NearDrop/IAP/GetPlusView.swift @@ -7,6 +7,7 @@ import SwiftUI import StoreKit +import NearbyShare let plusViewWidth: CGFloat = 440 let plusViewHeight: CGFloat = 200 @@ -28,7 +29,14 @@ struct GetPlusView: View { @State var buying = false @State var errorText = "" - @State var boughtSuccessAlert = false + @State var boughtSuccessAlert = false { + didSet { + if boughtSuccessAlert { + log("User bought plus version. Moving pending files...") + SaveFilesManager.shared.movePendingFilesToTarget() + } + } + } var body: some View { diff --git a/NearDrop/ar.lproj/Localizable.strings b/NearDrop/ar.lproj/Localizable.strings index d523085..134a388 100644 --- a/NearDrop/ar.lproj/Localizable.strings +++ b/NearDrop/ar.lproj/Localizable.strings @@ -80,7 +80,7 @@ "plusview_title" = "شكرًا لاستخدامك QuickDrop!"; "plusview_description" = "قم بفتح QuickDrop+ لمواصلة تبادل الملفات بلا حدود. QuickDrop+ هو شراء لمرة واحدة – لا حاجة للاشتراك. تشمل جميع الميزات الحالية والمستقبلية."; "plusview_success_title" = "شكرًا لشرائك!"; -"plusview_success_description" = "يمكنك الآن متابعة استقبال الملفات بدون قيود."; +"plusview_success_description" = "يمكنك الآن متابعة استلام الملفات بدون قيود. أي ملفات حاولت إرسالها سابقًا تم حفظها بالفعل على جهازك."; "plusview_success_button" = "حسنًا!"; "plusview_unlock_label" = "ادعم QuickDrop"; diff --git a/NearDrop/de.lproj/Localizable.strings b/NearDrop/de.lproj/Localizable.strings index 64b6b9b..2ee7925 100644 --- a/NearDrop/de.lproj/Localizable.strings +++ b/NearDrop/de.lproj/Localizable.strings @@ -88,7 +88,7 @@ Wenn du weiterhin Probleme hast, könnten diese durch deinen WLAN-Router verursa "plusview_title" = "Danke, dass du QuickDrop benutzt!"; "plusview_description" = "Schalte QuickDrop+ frei, um weiterhin Dateien unbegrenzt auszutauschen. QuickDrop+ ist ein Einmalkauf - ein Abonnement ist nicht erforderlich. Alle aktuellen und zukünftigen Funktionen sind enthalten."; "plusview_success_title" = "Danke für deinen Kauf!"; -"plusview_success_description" = "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen."; +"plusview_success_description" = "Du kannst nun weiterhin Dateien ohne Einschränkungen empfangen. Alle Dateien, welche gerade gesendet wurden, wurden bereits gespeichert."; "plusview_success_button" = "Alles klar!"; "plusview_unlock_label" = "Unterstütze QuickDrop"; diff --git a/NearDrop/es.lproj/Localizable.strings b/NearDrop/es.lproj/Localizable.strings index 0929507..0d00603 100644 --- a/NearDrop/es.lproj/Localizable.strings +++ b/NearDrop/es.lproj/Localizable.strings @@ -86,7 +86,7 @@ Si aún encuentras problemas, estos podrían ser causados por tu router WiFi. Al "plusview_title" = "¡Gracias por usar QuickDrop!"; "plusview_description" = "Desbloquea QuickDrop+ para seguir intercambiando archivos sin límites. QuickDrop+ es una compra única – no se requiere suscripción. Todas las funciones actuales y futuras están incluidas."; "plusview_success_title" = "¡Gracias por tu compra!"; -"plusview_success_description" = "Ahora puedes seguir recibiendo archivos sin restricciones."; +"plusview_success_description" = "Ahora puedes seguir recibiendo archivos sin restricciones. Todos los archivos que intentaste enviar antes ya se han guardado en tu dispositivo."; "plusview_success_button" = "¡Entendido!"; "plusview_unlock_label" = "Apoyar QuickDrop"; diff --git a/NearDrop/fr.lproj/Localizable.strings b/NearDrop/fr.lproj/Localizable.strings index 7857790..e65801b 100644 --- a/NearDrop/fr.lproj/Localizable.strings +++ b/NearDrop/fr.lproj/Localizable.strings @@ -85,7 +85,7 @@ Si vous rencontrez toujours des problèmes, ceux-ci peuvent être causés par vo "plusview_title" = "Merci d'utiliser QuickDrop !"; "plusview_description" = "Débloquez QuickDrop+ pour continuer à échanger des fichiers sans limites. QuickDrop+ est un achat unique – aucun abonnement requis. Toutes les fonctionnalités actuelles et futures sont incluses."; "plusview_success_title" = "Merci pour votre achat !"; -"plusview_success_description" = "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions."; +"plusview_success_description" = "Vous pouvez maintenant continuer à recevoir des fichiers sans restrictions. Tous les fichiers que vous avez essayé d'envoyer précédemment ont déjà été enregistrés sur votre appareil."; "plusview_success_button" = "D'accord !"; "plusview_unlock_label" = "Soutenir QuickDrop"; diff --git a/NearDrop/hu.lproj/Localizable.strings b/NearDrop/hu.lproj/Localizable.strings index ace2a49..099ba47 100644 --- a/NearDrop/hu.lproj/Localizable.strings +++ b/NearDrop/hu.lproj/Localizable.strings @@ -86,7 +86,7 @@ Ha továbbra is problémákat tapasztalsz, azok a WiFi-routeredből adódhatnak. "plusview_title" = "Köszönjük, hogy használod a QuickDropot!"; "plusview_description" = "Oldd fel a QuickDrop+-t, hogy korlátlanul folytathasd a fájlcserét. A QuickDrop+ egy egyszeri vásárlás – előfizetés nem szükséges. Az összes jelenlegi és jövőbeli funkciót tartalmazza."; "plusview_success_title" = "Köszönjük a vásárlást!"; -"plusview_success_description" = "Mostantól korlátozások nélkül fogadhatsz fájlokat."; +"plusview_success_description" = "Mostantól korlátozások nélkül fogadhat fájlokat. Az összes fájl, amelyet korábban próbált elküldeni, már el lett mentve az eszközére."; "plusview_success_button" = "Értettem!"; "plusview_unlock_label" = "Támogasd a QuickDropot"; diff --git a/NearDrop/it.lproj/Localizable.strings b/NearDrop/it.lproj/Localizable.strings index 8d9f34c..d7266ad 100644 --- a/NearDrop/it.lproj/Localizable.strings +++ b/NearDrop/it.lproj/Localizable.strings @@ -85,7 +85,7 @@ Se il problema persiste, potrebbe essere causato dal tuo router WiFi. Alcuni rou "plusview_title" = "Grazie per aver usato QuickDrop!"; "plusview_description" = "Sblocca QuickDrop+ per continuare a scambiare file senza limiti. QuickDrop+ è un acquisto unico – nessun abbonamento richiesto. Tutte le funzionalità attuali e future sono incluse."; "plusview_success_title" = "Grazie per il tuo acquisto!"; -"plusview_success_description" = "Ora puoi continuare a ricevere file senza restrizioni."; +"plusview_success_description" = "Ora puoi continuare a ricevere file senza restrizioni. Tutti i file che hai provato a inviare in precedenza sono già stati salvati sul tuo dispositivo."; "plusview_success_button" = "Capito!"; "plusview_unlock_label" = "Supporta QuickDrop"; diff --git a/NearDrop/ja.lproj/Localizable.strings b/NearDrop/ja.lproj/Localizable.strings index b9ad3e5..af7fb80 100644 --- a/NearDrop/ja.lproj/Localizable.strings +++ b/NearDrop/ja.lproj/Localizable.strings @@ -85,7 +85,7 @@ AndroidデバイスからMacにファイルを送信するには、スマート "plusview_title" = "QuickDropをご利用いただきありがとうございます!"; "plusview_description" = "QuickDrop+ をアンロックして、無制限にファイルを交換できます。QuickDrop+ は買い切り型のアプリです。サブスクリプション不要で、現在および将来のすべての機能が含まれます。"; "plusview_success_title" = "ご購入ありがとうございます!"; -"plusview_success_description" = "これで、制限なくファイルを受信できます。"; +"plusview_success_description" = "これで、制限なくファイルを受信し続けることができます。以前送信しようとしたファイルはすでにデバイスに保存されています。"; "plusview_success_button" = "了解!"; "plusview_unlock_label" = "QuickDropをサポート"; diff --git a/NearDrop/ko.lproj/Localizable.strings b/NearDrop/ko.lproj/Localizable.strings index dadff49..a817c0e 100644 --- a/NearDrop/ko.lproj/Localizable.strings +++ b/NearDrop/ko.lproj/Localizable.strings @@ -85,7 +85,7 @@ Android 기기에서 Mac으로 파일을 보내려면 휴대폰에서 공유 아 "plusview_title" = "QuickDrop을 사용해 주셔서 감사합니다!"; "plusview_description" = "QuickDrop+를 잠금 해제하여 파일을 제한 없이 교환하세요. QuickDrop+는 일회성 구매이며, 구독이 필요하지 않습니다. 모든 현재 및 미래 기능이 포함됩니다."; "plusview_success_title" = "구매해 주셔서 감사합니다!"; -"plusview_success_description" = "이제 제한 없이 파일을 받을 수 있습니다."; +"plusview_success_description" = "이제 파일을 제한 없이 계속 받을 수 있습니다. 이전에 전송을 시도한 모든 파일이 이미 기기에 저장되었습니다."; "plusview_success_button" = "알겠습니다!"; "plusview_unlock_label" = "QuickDrop 지원하기"; diff --git a/NearDrop/nl.lproj/Localizable.strings b/NearDrop/nl.lproj/Localizable.strings index a8698ff..63a4e7b 100644 --- a/NearDrop/nl.lproj/Localizable.strings +++ b/NearDrop/nl.lproj/Localizable.strings @@ -86,7 +86,7 @@ Als je nog steeds problemen ondervindt, kunnen deze veroorzaakt worden door je W "plusview_title" = "Bedankt voor het gebruiken van QuickDrop!"; "plusview_description" = "Ontgrendel QuickDrop+ om bestanden zonder limieten te blijven uitwisselen. QuickDrop+ is een eenmalige aankoop – geen abonnement vereist. Alle huidige en toekomstige functies zijn inbegrepen."; "plusview_success_title" = "Bedankt voor je aankoop!"; -"plusview_success_description" = "Je kunt nu bestanden blijven ontvangen zonder beperkingen."; +"plusview_success_description" = "Je kunt nu bestanden blijven ontvangen zonder beperkingen. Alle bestanden die je eerder probeerde te verzenden, zijn al opgeslagen op je apparaat."; "plusview_success_button" = "Begrepen!"; "plusview_unlock_label" = "Ondersteun QuickDrop"; diff --git a/NearDrop/pt-BR.lproj/Localizable.strings b/NearDrop/pt-BR.lproj/Localizable.strings index 2531ba8..6fdd83f 100644 --- a/NearDrop/pt-BR.lproj/Localizable.strings +++ b/NearDrop/pt-BR.lproj/Localizable.strings @@ -86,7 +86,7 @@ Se você ainda encontrar problemas, eles podem ser causados pelo seu roteador Wi "plusview_title" = "Obrigado por usar o QuickDrop!"; "plusview_description" = "Desbloqueie o QuickDrop+ para continuar trocando arquivos sem limites. QuickDrop+ é uma compra única – sem necessidade de assinatura. Todos os recursos atuais e futuros estão incluídos."; "plusview_success_title" = "Obrigado pela sua compra!"; -"plusview_success_description" = "Agora você pode continuar recebendo arquivos sem restrições."; +"plusview_success_description" = "Agora você pode continuar recebendo arquivos sem restrições. Todos os arquivos que você tentou enviar antes já foram salvos no seu dispositivo."; "plusview_success_button" = "Entendi!"; "plusview_unlock_label" = "Apoie o QuickDrop"; diff --git a/NearDrop/pt-PT.lproj/Localizable.strings b/NearDrop/pt-PT.lproj/Localizable.strings index 2d7cd57..5b224ad 100644 --- a/NearDrop/pt-PT.lproj/Localizable.strings +++ b/NearDrop/pt-PT.lproj/Localizable.strings @@ -85,7 +85,7 @@ Se ainda tiver problemas, estes podem ser causados pelo seu router WiFi. Alguns "plusview_title" = "Obrigado por usar o QuickDrop!"; "plusview_description" = "Desbloqueia o QuickDrop+ para continuar a trocar ficheiros sem limites. QuickDrop+ é uma compra única – não é necessária subscrição. Todas as funcionalidades atuais e futuras estão incluídas."; "plusview_success_title" = "Obrigado pela sua compra!"; -"plusview_success_description" = "Agora você pode continuar enviando arquivos sem restrições."; +"plusview_success_description" = "Agora pode continuar a receber ficheiros sem restrições. Todos os ficheiros que tentou enviar antes já foram guardados no seu dispositivo."; "plusview_success_button" = "Entendido!"; "plusview_unlock_label" = "Apoiar QuickDrop"; diff --git a/NearDrop/ro.lproj/Localizable.strings b/NearDrop/ro.lproj/Localizable.strings index 7b2c712..92b6cf6 100644 --- a/NearDrop/ro.lproj/Localizable.strings +++ b/NearDrop/ro.lproj/Localizable.strings @@ -85,7 +85,7 @@ Dacă întâmpini în continuare probleme, acestea ar putea fi cauzate de router "plusview_title" = "Mulțumim că folosești QuickDrop!"; "plusview_description" = "Deblochează QuickDrop+ pentru a continua schimbul de fișiere fără limite. QuickDrop+ este o achiziție unică – nu este necesar abonament. Toate funcțiile curente și viitoare sunt incluse."; "plusview_success_title" = "Mulțumim pentru achiziție!"; -"plusview_success_description" = "Acum poți continua să primești fișiere fără restricții."; +"plusview_success_description" = "Acum puteți continua să primiți fișiere fără restricții. Toate fișierele pe care ați încercat să le trimiteți anterior au fost deja salvate pe dispozitivul dvs."; "plusview_success_button" = "Înțeles!"; "plusview_unlock_label" = "Susține QuickDrop"; diff --git a/NearDrop/ru.lproj/Localizable.strings b/NearDrop/ru.lproj/Localizable.strings index 8233e0e..3a94c3d 100644 --- a/NearDrop/ru.lproj/Localizable.strings +++ b/NearDrop/ru.lproj/Localizable.strings @@ -87,7 +87,7 @@ "plusview_title" = "Спасибо за использование QuickDrop!"; "plusview_description" = "Разблокируйте QuickDrop+, чтобы продолжить обмен файлами без ограничений. QuickDrop+ – это разовая покупка, подписка не требуется. Все текущие и будущие функции включены."; "plusview_success_title" = "Спасибо за покупку!"; -"plusview_success_description" = "Теперь вы можете продолжать получать файлы без ограничений."; +"plusview_success_description" = "Теперь вы можете продолжать получать файлы без ограничений. Все файлы, которые вы пытались отправить ранее, уже сохранены на вашем устройстве."; "plusview_success_button" = "Понял!"; "plusview_unlock_label" = "Поддержать QuickDrop"; diff --git a/NearDrop/th.lproj/Localizable.strings b/NearDrop/th.lproj/Localizable.strings index 8ba301f..4d05c4f 100644 --- a/NearDrop/th.lproj/Localizable.strings +++ b/NearDrop/th.lproj/Localizable.strings @@ -82,7 +82,7 @@ "plusview_title" = "ขอบคุณที่ใช้ QuickDrop!"; "plusview_description" = "ปลดล็อก QuickDrop+ เพื่อแลกเปลี่ยนไฟล์ได้อย่างไม่จำกัด QuickDrop+ เป็นการซื้อครั้งเดียว – ไม่ต้องสมัครสมาชิก ฟีเจอร์ทั้งหมดทั้งปัจจุบันและอนาคตรวมอยู่ในนี้แล้ว"; "plusview_success_title" = "ขอบคุณสำหรับการซื้อ!"; -"plusview_success_description" = "คุณสามารถรับไฟล์ได้ต่อไปโดยไม่มีข้อจำกัดแล้ว"; +"plusview_success_description" = "คุณสามารถรับไฟล์ต่อไปได้โดยไม่มีข้อจำกัด ไฟล์ทั้งหมดที่คุณพยายามส่งก่อนหน้านี้ถูกบันทึกไว้ในอุปกรณ์ของคุณแล้ว"; "plusview_success_button" = "รับทราบ!"; "plusview_unlock_label" = "สนับสนุน QuickDrop"; diff --git a/NearDrop/uk.lproj/Localizable.strings b/NearDrop/uk.lproj/Localizable.strings index 2ce98a5..e058146 100644 --- a/NearDrop/uk.lproj/Localizable.strings +++ b/NearDrop/uk.lproj/Localizable.strings @@ -85,7 +85,7 @@ "plusview_title" = "Дякуємо за використання QuickDrop!"; "plusview_description" = "Розблокуйте QuickDrop+, щоб продовжити обмін файлами без обмежень. QuickDrop+ – це одноразова покупка, підписка не потрібна. Всі поточні та майбутні функції включені."; "plusview_success_title" = "Дякуємо за покупку!"; -"plusview_success_description" = "Тепер ви можете продовжувати отримувати файли без обмежень."; +"plusview_success_description" = "Тепер ви можете продовжувати отримувати файли без обмежень. Усі файли, які ви намагалися надіслати раніше, вже збережено на вашому пристрої."; "plusview_success_button" = "Зрозуміло!"; "plusview_unlock_label" = "Підтримати QuickDrop"; diff --git a/NearDrop/zh-Hans.lproj/Localizable.strings b/NearDrop/zh-Hans.lproj/Localizable.strings index 87b878a..2fb4430 100644 --- a/NearDrop/zh-Hans.lproj/Localizable.strings +++ b/NearDrop/zh-Hans.lproj/Localizable.strings @@ -85,7 +85,7 @@ "plusview_title" = "感谢使用 QuickDrop!"; "plusview_description" = "解锁 QuickDrop+ 以无限制地交换文件。QuickDrop+ 是一次性购买 – 无需订阅。所有当前和未来的功能均包含在内。"; "plusview_success_title" = "感谢您的购买!"; -"plusview_success_description" = "您现在可以无限制地接收文件了。"; +"plusview_success_description" = "您现在可以继续无限制地接收文件。您之前尝试发送的所有文件已保存到您的设备上。"; "plusview_success_button" = "知道了!"; "plusview_unlock_label" = "支持 QuickDrop"; diff --git a/NearbyShare/InboundNearbyConnection.swift b/NearbyShare/InboundNearbyConnection.swift index 41ab73d..b5de0ea 100644 --- a/NearbyShare/InboundNearbyConnection.swift +++ b/NearbyShare/InboundNearbyConnection.swift @@ -16,360 +16,509 @@ import SwiftECC import BigInt class InboundNearbyConnection: NearbyConnection{ - - private var currentState:State = .initial - public var delegate:InboundNearbyConnectionDelegate? - private var cipherCommitment:Data? - - private var textPayloadID:Int64=0 - private var securityScopeUrl: URL? - - enum State{ - case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected - } - - override init(connection: NWConnection, id:String) { - super.init(connection: connection, id: id) - } - - override func handleConnectionClosure() { - - super.handleConnectionClosure() - currentState = .disconnected - do{ - try deletePartiallyReceivedFiles() - }catch{ - log("Error deleting partially received files: \(error)") - } - DispatchQueue.main.async { - self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) - - if let url = self.securityScopeUrl { - log("Stopped accessing security scoped resource.") - url.stopAccessingSecurityScopedResource() - self.securityScopeUrl = nil - } - } - } - - override internal func processReceivedFrame(frameData:Data){ - do{ - switch currentState { - case .initial: - let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData) - try processConnectionRequestFrame(frame) - case .receivedConnectionRequest: - let msg=try Securegcm_Ukey2Message(serializedData: frameData) - ukeyClientInitMsgData=frameData - try processUkey2ClientInit(msg) - case .sentUkeyServerInit: - let msg=try Securegcm_Ukey2Message(serializedData: frameData) - try processUkey2ClientFinish(msg, raw: frameData) - case .receivedUkeyClientFinish: - let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData) - try processConnectionResponseFrame(frame) - default: - let smsg=try Securemessage_SecureMessage(serializedData: frameData) - try decryptAndProcessReceivedSecureMessage(smsg) - } - }catch{ - lastError=error - log("Deserialization error: \(error) in state \(currentState)") -#if !DEBUG - protocolError() -#endif - } - } - - override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{ - if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type { - log("Transfer canceled") - try sendDisconnectionAndDisconnect() - return - } - switch currentState{ - case .sentConnectionResponse: - try processPairedKeyEncryptionFrame(frame) - case .sentPairedKeyResult: - try processPairedKeyResultFrame(frame) - case .receivedPairedKeyResult: - try processIntroductionFrame(frame) - default: - log("Unexpected connection state in processTransferSetupFrame: \(currentState)") - log(frame.debugDescription) - } - } - - override func isServer() -> Bool { - return true - } - - override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws{ - let id=frame.payloadHeader.id - guard let fileInfo=transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") } - let currentOffset=fileInfo.bytesTransferred - guard frame.payloadChunk.offset==currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") } - guard currentOffset+Int64(frame.payloadChunk.body.count)<=fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") } - if frame.payloadChunk.body.count>0{ - fileInfo.fileHandle?.write(frame.payloadChunk.body) - transferredFiles[id]!.bytesTransferred+=Int64(frame.payloadChunk.body.count) - fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred - }else if (frame.payloadChunk.flags & 1)==1{ - try fileInfo.fileHandle?.close() - transferredFiles[id]!.fileHandle=nil - fileInfo.progress?.unpublish() - transferredFiles.removeValue(forKey: id) - if transferredFiles.isEmpty{ - try sendDisconnectionAndDisconnect() - } - } - } - - override func processBytesPayload(payload: Data, id: Int64) throws -> Bool { - if id==textPayloadID{ - if let urlStr=String(data: payload, encoding: .utf8), let url=URL(string: urlStr){ - NSWorkspace.shared.open(url) - } - try sendDisconnectionAndDisconnect() - return true - }else if let fileInfo=transferredFiles[id]{ - fileInfo.fileHandle?.write(payload) - transferredFiles[id]!.bytesTransferred+=Int64(payload.count) - fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred - try fileInfo.fileHandle?.close() - transferredFiles[id]!.fileHandle=nil - fileInfo.progress?.unpublish() - transferredFiles.removeValue(forKey: id) - try sendDisconnectionAndDisconnect() - return true - } - return false - } - - private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ - guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") } - guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") } - let endpointInfo=frame.v1.connectionRequest.endpointInfo - guard endpointInfo.count>17 else { throw NearbyError.protocolError("Endpoint info too short") } - let deviceNameLength=Int(endpointInfo[17]) - guard endpointInfo.count>=deviceNameLength+18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") } - guard let deviceName=String(data: endpointInfo[18..<(18+deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") } - let rawDeviceType:Int=Int(endpointInfo[0] & 7) >> 1 - remoteDeviceInfo=RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)) - currentState = .receivedConnectionRequest - } - - private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{ - guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") } - guard case .clientInit = msg.messageType else{ - sendUkey2Alert(type: .badMessageType) - throw NearbyError.ukey2 - } - let clientInit:Securegcm_Ukey2ClientInit - do{ - clientInit=try Securegcm_Ukey2ClientInit(serializedData: msg.messageData) - }catch{ - sendUkey2Alert(type: .badMessageData) - throw NearbyError.ukey2 - } - guard clientInit.version==1 else{ - sendUkey2Alert(type: .badVersion) - throw NearbyError.ukey2 - } - guard clientInit.random.count==32 else{ - sendUkey2Alert(type: .badRandom) - throw NearbyError.ukey2 - } - var found=false - for commitment in clientInit.cipherCommitments{ - if case .p256Sha512 = commitment.handshakeCipher{ - found=true - cipherCommitment=commitment.commitment - break - } - } - guard found else{ - sendUkey2Alert(type: .badHandshakeCipher) - throw NearbyError.ukey2 - } - guard clientInit.nextProtocol=="AES_256_CBC-HMAC_SHA256" else{ - sendUkey2Alert(type: .badNextProtocol) - throw NearbyError.ukey2 - } - - let domain=Domain.instance(curve: .EC256r1) - let (pubKey, privKey)=domain.makeKeyPair() - publicKey=pubKey - privateKey=privKey - - var serverInit=Securegcm_Ukey2ServerInit() - serverInit.version=1 - serverInit.random=Data.randomData(length: 32) - serverInit.handshakeCipher = .p256Sha512 - - var pkey=Securemessage_GenericPublicKey() - pkey.type = .ecP256 - pkey.ecP256PublicKey=Securemessage_EcP256PublicKey() - pkey.ecP256PublicKey.x=Data(pubKey.w.x.asSignedBytes()) - pkey.ecP256PublicKey.y=Data(pubKey.w.y.asSignedBytes()) - serverInit.publicKey=try pkey.serializedData() - - var serverInitMsg=Securegcm_Ukey2Message() - serverInitMsg.messageType = .serverInit - serverInitMsg.messageData=try serverInit.serializedData() - let serverInitData=try serverInitMsg.serializedData() - ukeyServerInitMsgData=serverInitData - sendFrameAsync(serverInitData) - currentState = .sentUkeyServerInit - } - - private func processUkey2ClientFinish(_ msg:Securegcm_Ukey2Message, raw:Data) throws{ - guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") } - guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 } - - var sha=SHA512() - sha.update(data: raw) - guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 } - - let clientFinish=try Securegcm_Ukey2ClientFinished(serializedData: msg.messageData) - guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") } - let clientKey=try Securemessage_GenericPublicKey(serializedData: clientFinish.publicKey) - - try finalizeKeyExchange(peerKey: clientKey) - - currentState = .receivedUkeyClientFinish - } - - private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ - guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") } - if case .connectionResponse = frame.v1.type { - var resp=Location_Nearby_Connections_OfflineFrame() - resp.version = .v1 - resp.v1=Location_Nearby_Connections_V1Frame() - resp.v1.type = .connectionResponse - resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame() - resp.v1.connectionResponse.response = .accept - resp.v1.connectionResponse.status=0 - resp.v1.connectionResponse.osInfo=Location_Nearby_Connections_OsInfo() - resp.v1.connectionResponse.osInfo.type = .apple - sendFrameAsync(try resp.serializedData()) - - encryptionDone=true - - var pairedEncryption=Sharing_Nearby_Frame() - pairedEncryption.version = .v1 - pairedEncryption.v1=Sharing_Nearby_V1Frame() - pairedEncryption.v1.type = .pairedKeyEncryption - pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame() - // Presumably used for all the phone number stuff that no one needs anyway - pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6) - pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72) - try sendTransferSetupFrame(pairedEncryption) - currentState = .sentConnectionResponse - } else { - log("Unhandled offline frame plaintext: \(frame)") - } - } - - private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{ - guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") } - var pairedResult=Sharing_Nearby_Frame() - pairedResult.version = .v1 - pairedResult.v1=Sharing_Nearby_V1Frame() - pairedResult.v1.type = .pairedKeyResult - pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame() - pairedResult.v1.pairedKeyResult.status = .unable - try sendTransferSetupFrame(pairedResult) - currentState = .sentPairedKeyResult - } - - private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{ - guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") } - currentState = .receivedPairedKeyResult - } - - private func makeFileDestinationURL(_ initialDest:URL) -> URL { - var dest=initialDest - if FileManager.default.fileExists(atPath: dest.path){ - var counter=1 - var path:String - let ext=dest.pathExtension - let baseUrl=dest.deletingPathExtension() - repeat{ - path="\(baseUrl.path) (\(counter))" - if !ext.isEmpty{ - path+=".\(ext)" - } - counter+=1 - }while FileManager.default.fileExists(atPath: path) - dest=URL(fileURLWithPath: path) - } - return dest - } - - private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{ - guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") } - currentState = .waitingForUserConsent - - if frame.v1.introduction.fileMetadata.count>0 && frame.v1.introduction.textMetadata.isEmpty{ - - let saveDirectory = getSaveDirectory() - - for file in frame.v1.introduction.fileMetadata { - - let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name)) - let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType), - payloadID: file.payloadID, - destinationURL: dest) - transferredFiles[file.payloadID] = info - } - let metadata=TransferMetadata(files: transferredFiles.map({$0.value.meta}), id: id, pinCode: pinCode) - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) - } - }else if frame.v1.introduction.textMetadata.count==1{ - let meta=frame.v1.introduction.textMetadata[0] - if case .url=meta.type{ - let metadata=TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle) - textPayloadID=meta.payloadID - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) - } - }else if case .text=meta.type{ - - let saveDirectory = getSaveDirectory() - - let dateFormatter=DateFormatter() - dateFormatter.dateFormat="yyyy-MM-dd HH.mm.ss" - let dest=makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt")) - let info=InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"), - payloadID: meta.payloadID, - destinationURL: dest) - transferredFiles[meta.payloadID]=info - DispatchQueue.main.async { - self.delegate?.obtainUserConsent(for: TransferMetadata(files: [info.meta], id: self.id, pinCode: self.pinCode), from: self.remoteDeviceInfo!, connection: self) - } - }else{ - rejectTransfer(with: .unsupportedAttachmentType) - } - }else{ - rejectTransfer(with: .unsupportedAttachmentType) - } - } - private func getSaveDirectory() -> URL { + private var currentState:State = .initial + public var delegate:InboundNearbyConnectionDelegate? + private var cipherCommitment:Data? + + private var textPayloadID:Int64=0 + + enum State{ + case initial, receivedConnectionRequest, sentUkeyServerInit, receivedUkeyClientFinish, sentConnectionResponse, sentPairedKeyResult, receivedPairedKeyResult, waitingForUserConsent, receivingFiles, disconnected + } + + override init(connection: NWConnection, id:String) { + super.init(connection: connection, id: id) + } + + override func handleConnectionClosure() { + + super.handleConnectionClosure() + currentState = .disconnected + do { + try deletePartiallyReceivedFiles() + } catch{ + log("Error deleting partially received files: \(error)") + } + DispatchQueue.main.async { + self.delegate?.connectionWasTerminated(connection: self, error: self.lastError) + + SaveFilesManager.shared.stopAccessingSecurityScopedResource() + } + } + + override internal func processReceivedFrame(frameData:Data){ + do{ + switch currentState { + case .initial: + let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData) + try processConnectionRequestFrame(frame) + case .receivedConnectionRequest: + let msg=try Securegcm_Ukey2Message(serializedData: frameData) + ukeyClientInitMsgData=frameData + try processUkey2ClientInit(msg) + case .sentUkeyServerInit: + let msg=try Securegcm_Ukey2Message(serializedData: frameData) + try processUkey2ClientFinish(msg, raw: frameData) + case .receivedUkeyClientFinish: + let frame=try Location_Nearby_Connections_OfflineFrame(serializedData: frameData) + try processConnectionResponseFrame(frame) + default: + let smsg=try Securemessage_SecureMessage(serializedData: frameData) + try decryptAndProcessReceivedSecureMessage(smsg) + } + }catch{ + lastError=error + log("Deserialization error: \(error) in state \(currentState)") +#if !DEBUG + protocolError() +#endif + } + } + + override internal func processTransferSetupFrame(_ frame:Sharing_Nearby_Frame) throws{ + if frame.hasV1 && frame.v1.hasType, case .cancel = frame.v1.type { + log("Transfer canceled") + try sendDisconnectionAndDisconnect() + return + } + switch currentState{ + case .sentConnectionResponse: + try processPairedKeyEncryptionFrame(frame) + case .sentPairedKeyResult: + try processPairedKeyResultFrame(frame) + case .receivedPairedKeyResult: + try processIntroductionFrame(frame) + default: + log("Unexpected connection state in processTransferSetupFrame: \(currentState)") + log(frame.debugDescription) + } + } + + override func isServer() -> Bool { + return true + } + + override func processFileChunk(frame: Location_Nearby_Connections_PayloadTransferFrame) throws{ + let id=frame.payloadHeader.id + guard let fileInfo=transferredFiles[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") } + let currentOffset=fileInfo.bytesTransferred + guard frame.payloadChunk.offset==currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") } + guard currentOffset+Int64(frame.payloadChunk.body.count)<=fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") } + if frame.payloadChunk.body.count>0{ + fileInfo.fileHandle?.write(frame.payloadChunk.body) + transferredFiles[id]!.bytesTransferred+=Int64(frame.payloadChunk.body.count) + fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred + }else if (frame.payloadChunk.flags & 1)==1{ + try fileInfo.fileHandle?.close() + transferredFiles[id]!.fileHandle=nil + fileInfo.progress?.unpublish() + transferredFiles.removeValue(forKey: id) + if transferredFiles.isEmpty{ + try sendDisconnectionAndDisconnect() + } + } + } + + override func processBytesPayload(payload: Data, id: Int64) throws -> Bool { + if id==textPayloadID{ + if let urlStr=String(data: payload, encoding: .utf8), let url=URL(string: urlStr){ + NSWorkspace.shared.open(url) + } + try sendDisconnectionAndDisconnect() + return true + }else if let fileInfo=transferredFiles[id]{ + fileInfo.fileHandle?.write(payload) + transferredFiles[id]!.bytesTransferred+=Int64(payload.count) + fileInfo.progress?.completedUnitCount=transferredFiles[id]!.bytesTransferred + try fileInfo.fileHandle?.close() + transferredFiles[id]!.fileHandle=nil + fileInfo.progress?.unpublish() + transferredFiles.removeValue(forKey: id) + try sendDisconnectionAndDisconnect() + return true + } + return false + } + + private func processConnectionRequestFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ + guard frame.hasV1 && frame.v1.hasConnectionRequest && frame.v1.connectionRequest.hasEndpointInfo else { throw NearbyError.requiredFieldMissing("connectionRequest.endpointInfo") } + guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") } + let endpointInfo=frame.v1.connectionRequest.endpointInfo + guard endpointInfo.count>17 else { throw NearbyError.protocolError("Endpoint info too short") } + let deviceNameLength=Int(endpointInfo[17]) + guard endpointInfo.count>=deviceNameLength+18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") } + guard let deviceName=String(data: endpointInfo[18..<(18+deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") } + let rawDeviceType:Int=Int(endpointInfo[0] & 7) >> 1 + remoteDeviceInfo=RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType)) + currentState = .receivedConnectionRequest + } + + private func processUkey2ClientInit(_ msg:Securegcm_Ukey2Message) throws{ + guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientInit ukey2message.type|data") } + guard case .clientInit = msg.messageType else{ + sendUkey2Alert(type: .badMessageType) + throw NearbyError.ukey2 + } + let clientInit:Securegcm_Ukey2ClientInit + do{ + clientInit=try Securegcm_Ukey2ClientInit(serializedData: msg.messageData) + }catch{ + sendUkey2Alert(type: .badMessageData) + throw NearbyError.ukey2 + } + guard clientInit.version==1 else{ + sendUkey2Alert(type: .badVersion) + throw NearbyError.ukey2 + } + guard clientInit.random.count==32 else{ + sendUkey2Alert(type: .badRandom) + throw NearbyError.ukey2 + } + var found=false + for commitment in clientInit.cipherCommitments{ + if case .p256Sha512 = commitment.handshakeCipher{ + found=true + cipherCommitment=commitment.commitment + break + } + } + guard found else{ + sendUkey2Alert(type: .badHandshakeCipher) + throw NearbyError.ukey2 + } + guard clientInit.nextProtocol=="AES_256_CBC-HMAC_SHA256" else{ + sendUkey2Alert(type: .badNextProtocol) + throw NearbyError.ukey2 + } + + let domain=Domain.instance(curve: .EC256r1) + let (pubKey, privKey)=domain.makeKeyPair() + publicKey=pubKey + privateKey=privKey + + var serverInit=Securegcm_Ukey2ServerInit() + serverInit.version=1 + serverInit.random=Data.randomData(length: 32) + serverInit.handshakeCipher = .p256Sha512 + + var pkey=Securemessage_GenericPublicKey() + pkey.type = .ecP256 + pkey.ecP256PublicKey=Securemessage_EcP256PublicKey() + pkey.ecP256PublicKey.x=Data(pubKey.w.x.asSignedBytes()) + pkey.ecP256PublicKey.y=Data(pubKey.w.y.asSignedBytes()) + serverInit.publicKey=try pkey.serializedData() + + var serverInitMsg=Securegcm_Ukey2Message() + serverInitMsg.messageType = .serverInit + serverInitMsg.messageData=try serverInit.serializedData() + let serverInitData=try serverInitMsg.serializedData() + ukeyServerInitMsgData=serverInitData + sendFrameAsync(serverInitData) + currentState = .sentUkeyServerInit + } + + private func processUkey2ClientFinish(_ msg:Securegcm_Ukey2Message, raw:Data) throws{ + guard msg.hasMessageType, msg.hasMessageData else { throw NearbyError.requiredFieldMissing("clientFinish ukey2message.type|data") } + guard case .clientFinish = msg.messageType else { throw NearbyError.ukey2 } + + var sha=SHA512() + sha.update(data: raw) + guard cipherCommitment==Data(sha.finalize()) else { throw NearbyError.ukey2 } + + let clientFinish=try Securegcm_Ukey2ClientFinished(serializedData: msg.messageData) + guard clientFinish.hasPublicKey else {throw NearbyError.requiredFieldMissing("ukey2clientFinish.publicKey") } + let clientKey=try Securemessage_GenericPublicKey(serializedData: clientFinish.publicKey) + + try finalizeKeyExchange(peerKey: clientKey) + + currentState = .receivedUkeyClientFinish + } + + private func processConnectionResponseFrame(_ frame:Location_Nearby_Connections_OfflineFrame) throws{ + guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") } + if case .connectionResponse = frame.v1.type { + var resp=Location_Nearby_Connections_OfflineFrame() + resp.version = .v1 + resp.v1=Location_Nearby_Connections_V1Frame() + resp.v1.type = .connectionResponse + resp.v1.connectionResponse=Location_Nearby_Connections_ConnectionResponseFrame() + resp.v1.connectionResponse.response = .accept + resp.v1.connectionResponse.status=0 + resp.v1.connectionResponse.osInfo=Location_Nearby_Connections_OsInfo() + resp.v1.connectionResponse.osInfo.type = .apple + sendFrameAsync(try resp.serializedData()) + + encryptionDone=true + + var pairedEncryption=Sharing_Nearby_Frame() + pairedEncryption.version = .v1 + pairedEncryption.v1=Sharing_Nearby_V1Frame() + pairedEncryption.v1.type = .pairedKeyEncryption + pairedEncryption.v1.pairedKeyEncryption=Sharing_Nearby_PairedKeyEncryptionFrame() + // Presumably used for all the phone number stuff that no one needs anyway + pairedEncryption.v1.pairedKeyEncryption.secretIDHash=Data.randomData(length: 6) + pairedEncryption.v1.pairedKeyEncryption.signedData=Data.randomData(length: 72) + try sendTransferSetupFrame(pairedEncryption) + currentState = .sentConnectionResponse + } else { + log("Unhandled offline frame plaintext: \(frame)") + } + } + + private func processPairedKeyEncryptionFrame(_ frame:Sharing_Nearby_Frame) throws{ + guard frame.hasV1, frame.v1.hasPairedKeyEncryption else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption") } + var pairedResult=Sharing_Nearby_Frame() + pairedResult.version = .v1 + pairedResult.v1=Sharing_Nearby_V1Frame() + pairedResult.v1.type = .pairedKeyResult + pairedResult.v1.pairedKeyResult=Sharing_Nearby_PairedKeyResultFrame() + pairedResult.v1.pairedKeyResult.status = .unable + try sendTransferSetupFrame(pairedResult) + currentState = .sentPairedKeyResult + } + + private func processPairedKeyResultFrame(_ frame:Sharing_Nearby_Frame) throws{ + guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") } + currentState = .receivedPairedKeyResult + } + + private func makeFileDestinationURL(_ initialDest:URL) -> URL { + var dest=initialDest + if FileManager.default.fileExists(atPath: dest.path){ + var counter=1 + var path:String + let ext=dest.pathExtension + let baseUrl=dest.deletingPathExtension() + repeat{ + path="\(baseUrl.path) (\(counter))" + if !ext.isEmpty{ + path+=".\(ext)" + } + counter+=1 + }while FileManager.default.fileExists(atPath: path) + dest=URL(fileURLWithPath: path) + } + return dest + } + + private func processIntroductionFrame(_ frame:Sharing_Nearby_Frame) throws{ + guard frame.hasV1, frame.v1.hasIntroduction else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction") } + currentState = .waitingForUserConsent + + if frame.v1.introduction.fileMetadata.count>0 && frame.v1.introduction.textMetadata.isEmpty{ + + let saveDirectory = SaveFilesManager.shared.getSaveDirectory() + + for file in frame.v1.introduction.fileMetadata { + + let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name)) + let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType), + payloadID: file.payloadID, + destinationURL: dest) + transferredFiles[file.payloadID] = info + } + let metadata=TransferMetadata(files: transferredFiles.map({$0.value.meta}), id: id, pinCode: pinCode) + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) + } + }else if frame.v1.introduction.textMetadata.count==1{ + let meta=frame.v1.introduction.textMetadata[0] + if case .url=meta.type{ + let metadata=TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: meta.textTitle) + textPayloadID=meta.payloadID + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(for: metadata, from: self.remoteDeviceInfo!, connection: self) + } + }else if case .text=meta.type{ + + let saveDirectory = SaveFilesManager.shared.getSaveDirectory() + + let dateFormatter=DateFormatter() + dateFormatter.dateFormat="yyyy-MM-dd HH.mm.ss" + let dest=makeFileDestinationURL(saveDirectory.appendingPathComponent("\(dateFormatter.string(from: Date())).txt")) + let info=InternalFileInfo(meta: FileMetadata(name: dest.lastPathComponent, size: meta.size, mimeType: "text/plain"), + payloadID: meta.payloadID, + destinationURL: dest) + transferredFiles[meta.payloadID]=info + DispatchQueue.main.async { + self.delegate?.obtainUserConsent(for: TransferMetadata(files: [info.meta], id: self.id, pinCode: self.pinCode), from: self.remoteDeviceInfo!, connection: self) + } + }else{ + rejectTransfer(with: .unsupportedAttachmentType) + } + }else{ + rejectTransfer(with: .unsupportedAttachmentType) + } + } + + func submitUserConsent(accepted:Bool, storeInTemp: Bool = false){ + DispatchQueue.global(qos: .utility).async { + if accepted{ + self.acceptTransfer(storeInTemp: storeInTemp) + }else{ + self.rejectTransfer() + } + } + } + + private func acceptTransfer(storeInTemp: Bool) { + + if currentState == .disconnected { + log("Detected timeout, not accepting transfer") + return + } + + do { + if storeInTemp { + try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true) + } + + for (id, file) in transferredFiles { + let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL + + FileManager.default.createFile(atPath: targetURL.path, contents: nil) + let handle = try FileHandle(forWritingTo: targetURL) + transferredFiles[id]!.fileHandle = handle + + let progress = Progress() + progress.fileURL = targetURL + progress.totalUnitCount = file.meta.size + progress.kind = .file + progress.isPausable = false + progress.publish() + transferredFiles[id]!.progress = progress + transferredFiles[id]!.created = true + } + + var frame = Sharing_Nearby_Frame() + frame.version = .v1 + frame.v1.type = .response + frame.v1.connectionResponse.status = .accept + currentState = .receivingFiles + try sendTransferSetupFrame(frame) + } catch { + lastError = error + protocolError() + } + } + + + private func rejectTransfer(with reason:Sharing_Nearby_ConnectionResponseFrame.Status = .reject){ + var frame=Sharing_Nearby_Frame() + frame.version = .v1 + frame.v1.type = .response + frame.v1.connectionResponse.status = reason + do{ + try sendTransferSetupFrame(frame) + try sendDisconnectionAndDisconnect() + }catch{ + log("Error \(error)") + protocolError() + } + } + + + private func deletePartiallyReceivedFiles() throws{ + for (_, file) in transferredFiles { + guard file.created else { continue } + try FileManager.default.removeItem(at: file.destinationURL) + } + } +} + +protocol InboundNearbyConnectionDelegate{ + func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection) + func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?) +} + + +public class SaveFilesManager { + + private init() {} + + public static let shared = SaveFilesManager() + + internal let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending") + private var securityScopeUrl: URL? + + public func movePendingFilesToTarget() { + + log("Moving pending files to target directory") + + do { + let fileManager = FileManager.default + let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil) + + let target = getSaveDirectory() + + for file in files { + let fileName = file.lastPathComponent + var destinationURL = target.appendingPathComponent(fileName) + + var counter = 1 + let fileExtension = destinationURL.pathExtension + let baseName = destinationURL.deletingPathExtension().lastPathComponent + + while fileManager.fileExists(atPath: destinationURL.path) { + let newFileName = "\(baseName)-\(counter)" + let newFileNameWithExtension = fileExtension.isEmpty ? newFileName : "\(newFileName).\(fileExtension)" + destinationURL = target.appendingPathComponent(newFileNameWithExtension) + counter += 1 + } + + log("Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)") + try fileManager.copyItem(at: file, to: destinationURL) + } + + try fileManager.removeItem(at: tempDirectory) + + stopAccessingSecurityScopedResource() + + log("Moved all pending files to target directory") + + } catch { + log("Error moving pending files: \(error)") + } + } + + + public func stopAccessingSecurityScopedResource() { + guard let url = securityScopeUrl else { + return + } + + log("Stopping access to security scoped resource: \(url)") + url.stopAccessingSecurityScopedResource() + self.securityScopeUrl = nil + } + + + public func getSaveDirectory() -> URL { + + if let securityScopeUrl = securityScopeUrl { + log("Using existing security scope URL: \(securityScopeUrl)") + return securityScopeUrl + } + if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) { + var isStale = false + do { let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) - if !isStale { - if url.startAccessingSecurityScopedResource() { - self.securityScopeUrl = url - return url - } - } else { - print("Bookmark is stale, using default downloads folder.") + + if !isStale { + if url.startAccessingSecurityScopedResource() { + + log("Successfully accessed security scoped resource: \(url)") + + self.securityScopeUrl = url + return url } + } else { + print("Bookmark is stale, using default downloads folder.") + } } catch { print("Failed to resolve bookmark: \(error), using default downloads folder.") @@ -382,74 +531,4 @@ class InboundNearbyConnection: NearbyConnection{ fatalError("Failed to get downloads directory: \(error)") } } - - func submitUserConsent(accepted:Bool){ - DispatchQueue.global(qos: .utility).async { - if accepted{ - self.acceptTransfer() - }else{ - self.rejectTransfer() - } - } - } - - private func acceptTransfer(){ - - if currentState == .disconnected { - log("Detected timeout, not accepting transfer") - return - } - - do{ - for (id, file) in transferredFiles{ - FileManager.default.createFile(atPath: file.destinationURL.path, contents: nil) - let handle=try FileHandle(forWritingTo: file.destinationURL) - transferredFiles[id]!.fileHandle=handle - let progress=Progress() - progress.fileURL=file.destinationURL - progress.totalUnitCount=file.meta.size - progress.kind = .file - progress.isPausable=false - progress.publish() - transferredFiles[id]!.progress=progress - transferredFiles[id]!.created=true - } - - var frame=Sharing_Nearby_Frame() - frame.version = .v1 - frame.v1.type = .response - frame.v1.connectionResponse.status = .accept - currentState = .receivingFiles - try sendTransferSetupFrame(frame) - }catch{ - lastError=error - protocolError() - } - } - - private func rejectTransfer(with reason:Sharing_Nearby_ConnectionResponseFrame.Status = .reject){ - var frame=Sharing_Nearby_Frame() - frame.version = .v1 - frame.v1.type = .response - frame.v1.connectionResponse.status = reason - do{ - try sendTransferSetupFrame(frame) - try sendDisconnectionAndDisconnect() - }catch{ - log("Error \(error)") - protocolError() - } - } - - private func deletePartiallyReceivedFiles() throws{ - for (_, file) in transferredFiles{ - guard file.created else { continue } - try FileManager.default.removeItem(at: file.destinationURL) - } - } -} - -protocol InboundNearbyConnectionDelegate{ - func obtainUserConsent(for transfer:TransferMetadata, from device:RemoteDeviceInfo, connection:InboundNearbyConnection) - func connectionWasTerminated(connection:InboundNearbyConnection, error:Error?) } diff --git a/NearbyShare/NearbyConnectionManager.swift b/NearbyShare/NearbyConnectionManager.swift index c9d29dd..22262f0 100644 --- a/NearbyShare/NearbyConnectionManager.swift +++ b/NearbyShare/NearbyConnectionManager.swift @@ -228,9 +228,9 @@ public class NearbyConnectionManager : NSObject, NetServiceDelegate, InboundNear activeConnections.removeValue(forKey: connection.id) } - public func submitUserConsent(transferID:String, accept:Bool){ + public func submitUserConsent(transferID:String, accept:Bool, storeInTemp: Bool = false){ guard let conn=activeConnections[transferID] else {return} - conn.submitUserConsent(accepted: accept) + conn.submitUserConsent(accepted: accept, storeInTemp: storeInTemp) } public func startDeviceDiscovery(){