Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d019e109 | ||
|
|
606446a1b9 | ||
|
|
f6588c3c43 | ||
|
|
28178a7e6a | ||
|
|
e9b6a477fc | ||
|
|
7c35cc7b35 | ||
|
|
664db36267 | ||
|
|
167542f0fa | ||
|
|
cff6b8633f | ||
|
|
295b97278c | ||
|
|
7d82520c2f | ||
|
|
d98be9c23c | ||
|
|
9de9ebb4ba | ||
|
|
b5de24d926 | ||
|
|
28e0ede409 | ||
|
|
dd9462d9f5 | ||
|
|
238769656a | ||
|
|
fd29254e38 | ||
|
|
6b0e14de3e | ||
|
|
db70d40e43 | ||
|
|
4394b78242 | ||
|
|
488f1a14b2 | ||
|
|
72bfd88161 | ||
|
|
2f6b0ff5ba | ||
|
|
5fdc5f8a1b | ||
|
|
ee091f60cd | ||
|
|
6c00c0f321 | ||
|
|
65a0ab3226 | ||
|
|
f5ca60a0ad | ||
|
|
ce16bca415 | ||
|
|
e6d1c7b1a3 | ||
|
|
84d2150893 | ||
|
|
007784c483 | ||
|
|
78cf2309f9 | ||
|
|
976fc280db | ||
|
|
02ab04c69e | ||
|
|
fcbe99dd85 | ||
|
|
feaf557ca2 | ||
|
|
9a488a2ae2 | ||
|
|
80b81ae228 | ||
|
|
0112e512c0 |
No files matched your search
@@ -2,3 +2,4 @@ DerivedData/
|
|||||||
.DerivedData/
|
.DerivedData/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
/research/
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 1019 KiB |
|
After Width: | Height: | Size: 850 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 777 KiB |
@@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"forceMultiLine" : true,
|
||||||
|
"screenshots" : [
|
||||||
|
{
|
||||||
|
"baseText" : "Quick Transfer to Android & more.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "1.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "1",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "نقل سريع إلى أندرويد والمزيد.",
|
||||||
|
"ca" : "Transferència ràpida a Android i més.",
|
||||||
|
"cs" : "Rychlý přenos na Android i jinam.",
|
||||||
|
"da" : "Hurtig filoverførsel til Android m.m.",
|
||||||
|
"de-DE" : "Schneller Transfer zu Android & mehr.",
|
||||||
|
"el" : "Γρήγορη μεταφορά σε Android και άλλα.",
|
||||||
|
"en-AU" : "Quick Transfer to Android & more.",
|
||||||
|
"en-CA" : "Quick Transfer to Android & more.",
|
||||||
|
"en-GB" : "Quick Transfer to Android & more.",
|
||||||
|
"es-ES" : "Transferencia rápida a Android y más.",
|
||||||
|
"es-MX" : "Transferencia rápida a Android y más.",
|
||||||
|
"fi" : "Nopea tiedostonsiirto Androidille ym.",
|
||||||
|
"fr-CA" : "Transfert rapide vers Android+.",
|
||||||
|
"fr-FR" : "Transfert rapide vers Android+.",
|
||||||
|
"he" : "העברת קבצים מהירה לאנדרואיד ועוד.",
|
||||||
|
"hi" : "एंड्रॉइड आदि पर तेज़ फ़ाइल ट्रांसफर।",
|
||||||
|
"hr" : "Brz prijenos na Android i više.",
|
||||||
|
"hu" : "Gyors fájlátvitel Androidra és másra.",
|
||||||
|
"id" : "Transfer cepat ke Android & lainnya.",
|
||||||
|
"it" : "Trasferisci file su Android e altro.",
|
||||||
|
"ja" : "Android などへのファイルを素早く転送。",
|
||||||
|
"ko" : "Android 등으로 빠른 파일 전송.",
|
||||||
|
"ms" : "Pindahan pantas ke Android+.",
|
||||||
|
"nl-NL" : "Snel bestanden naar Android en meer.",
|
||||||
|
"no" : "Rask overføring til Android og mer.",
|
||||||
|
"pl" : "Szybki transfer na Androida i więcej.",
|
||||||
|
"pt-BR" : "Transfer rápido para Android e mais.",
|
||||||
|
"pt-PT" : "Transfer rápido para Android e mais.",
|
||||||
|
"ro" : "Transfer rapid la Android și altele.",
|
||||||
|
"ru" : "Передача на Android и не только.",
|
||||||
|
"sk" : "Rýchly prenos na Android aj inde.",
|
||||||
|
"sv" : "Snabb överföring till Android m.m.",
|
||||||
|
"th" : "ถ่ายโอนไฟล์เร็วไปยัง Android และอื่นๆ",
|
||||||
|
"tr" : "Android'e hızlı aktarım ve fazlası.",
|
||||||
|
"uk" : "Передача на Android і не тільки.",
|
||||||
|
"vi" : "Chuyển nhanh sang Android và hơn nữa.",
|
||||||
|
"zh-Hans" : "快速文件传输到安卓及更多设备。",
|
||||||
|
"zh-Hant" : "快速檔案傳輸到 Android 及更多裝置。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Browse & manage received files.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "2.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "2",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "تصفح وأدر الملفات المستلمة.",
|
||||||
|
"ca" : "Navega i gestiona els fitxers rebuts.",
|
||||||
|
"cs" : "Přijaté soubory.",
|
||||||
|
"da" : "Se og administrer modtagne filer.",
|
||||||
|
"de-DE" : "Empfangene Dateien ansehen & ordnen.",
|
||||||
|
"el" : "Δες και διαχειρίσου ληφθέντα αρχεία.",
|
||||||
|
"en-AU" : "Browse & manage received files.",
|
||||||
|
"en-CA" : "Browse & manage received files.",
|
||||||
|
"en-GB" : "Browse & manage received files.",
|
||||||
|
"es-ES" : "Archivos recibidos: ver y gestionar.",
|
||||||
|
"es-MX" : "Archivos recibidos: ver y gestionar.",
|
||||||
|
"fi" : "Selaa ja hallitse saatuja tiedostoja.",
|
||||||
|
"fr-CA" : "Parcourez et gérez vos fichiers.",
|
||||||
|
"fr-FR" : "Parcourez et gérez vos fichiers.",
|
||||||
|
"he" : "עיין ונהל קבצים שהתקבלו.",
|
||||||
|
"hi" : "प्राप्त फ़ाइलें देखें व संभालें।",
|
||||||
|
"hr" : "Primljene datoteke.",
|
||||||
|
"hu" : "Fogadott fájlok.",
|
||||||
|
"id" : "Jelajahi & kelola file yang diterima.",
|
||||||
|
"it" : "Sfoglia e gestisci i file ricevuti.",
|
||||||
|
"ja" : "受信ファイルを閲覧&管理",
|
||||||
|
"ko" : "받은 파일을 찾아보고 관리하세요.",
|
||||||
|
"ms" : "Semak & urus fail diterima.",
|
||||||
|
"nl-NL" : "Beheer ontvangen bestanden.",
|
||||||
|
"no" : "Se og administrer mottatte filer.",
|
||||||
|
"pl" : "Odebrane pliki: przegląd i obsługa.",
|
||||||
|
"pt-BR" : "Veja e gerencie arquivos recebidos.",
|
||||||
|
"pt-PT" : "Gere ficheiros recebidos.",
|
||||||
|
"ro" : "Fișiere primite: vezi și gestionează.",
|
||||||
|
"ru" : "Полученные файлы: обзор и управление.",
|
||||||
|
"sk" : "Prehliadaj a spravuj prijaté súbory.",
|
||||||
|
"sv" : "Bläddra och hantera mottagna filer.",
|
||||||
|
"th" : "เรียกดูและจัดการไฟล์ที่ได้รับ",
|
||||||
|
"tr" : "Alınan dosyalara göz atın ve yönetin.",
|
||||||
|
"uk" : "Отримані файли.",
|
||||||
|
"vi" : "Duyệt và quản lý các tệp đã nhận.",
|
||||||
|
"zh-Hans" : "浏览和管理收到的文件。",
|
||||||
|
"zh-Hant" : "瀏覽並管理已接收的檔案。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Share files right from your apps.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "3.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "3",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "شارك الملفات مباشرة من تطبيقاتك.",
|
||||||
|
"ca" : "Comparteix fitxers des de les apps.",
|
||||||
|
"cs" : "Sdílejte soubory přímo z aplikací.",
|
||||||
|
"da" : "Del filer direkte fra dine apps.",
|
||||||
|
"de-DE" : "Teile Dateien direkt aus deinen Apps.",
|
||||||
|
"el" : "Μοιράσου αρχεία από τις εφαρμογές.",
|
||||||
|
"en-AU" : "Share files right from your apps.",
|
||||||
|
"en-CA" : "Share files right from your apps.",
|
||||||
|
"en-GB" : "Share files right from your apps.",
|
||||||
|
"es-ES" : "Comparte archivos desde tus apps.",
|
||||||
|
"es-MX" : "Comparte archivos desde tus apps.",
|
||||||
|
"fi" : "Jaa tiedostoja sovelluksistasi.",
|
||||||
|
"fr-CA" : "Partagez des fichiers via vos apps.",
|
||||||
|
"fr-FR" : "Partagez des fichiers via vos apps.",
|
||||||
|
"he" : "שתף קבצים ישירות מהאפליקציות שלך.",
|
||||||
|
"hi" : "अपने ऐप्स से सीधे फ़ाइलें साझा करें।",
|
||||||
|
"hr" : "Dijeli datoteke iz svojih aplikacija.",
|
||||||
|
"hu" : "Ossz meg fájlokat az appjaidból.",
|
||||||
|
"id" : "Bagikan file dari aplikasimu.",
|
||||||
|
"it" : "Condividi file dalle tue app.",
|
||||||
|
"ja" : "アプリから直接ファイルを共有できます。",
|
||||||
|
"ko" : "앱에서 바로 파일을 공유하세요.",
|
||||||
|
"ms" : "Kongsi fail terus dari aplikasi anda.",
|
||||||
|
"nl-NL" : "Deel bestanden direct vanuit je apps.",
|
||||||
|
"no" : "Del filer direkte fra appene dine.",
|
||||||
|
"pl" : "Udostępniaj pliki z aplikacji.",
|
||||||
|
"pt-BR" : "Compartilhe arquivos direto dos apps.",
|
||||||
|
"pt-PT" : "Partilha ficheiros das tuas apps.",
|
||||||
|
"ro" : "Partajează fișiere din aplicații.",
|
||||||
|
"ru" : "Делитесь файлами из приложений.",
|
||||||
|
"sk" : "Zdieľaj súbory zo svojich aplikácií.",
|
||||||
|
"sv" : "Dela filer direkt från dina appar.",
|
||||||
|
"th" : "แชร์ไฟล์ได้โดยตรงจากแอปของคุณ",
|
||||||
|
"tr" : "Uygulamalardan paylaş.",
|
||||||
|
"uk" : "Діліться файлами прямо з додатків.",
|
||||||
|
"vi" : "Chia sẻ tệp ngay từ ứng dụng của bạn.",
|
||||||
|
"zh-Hans" : "直接从你的应用中分享文件。",
|
||||||
|
"zh-Hant" : "直接從你的應用程式分享檔案。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"baseText" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"color" : {
|
||||||
|
"alpha" : 1,
|
||||||
|
"blue" : 0.19851559400558472,
|
||||||
|
"green" : 0.19851559400558472,
|
||||||
|
"red" : 0.19851559400558472
|
||||||
|
},
|
||||||
|
"fileName" : "4.png",
|
||||||
|
"fontSize" : 107,
|
||||||
|
"key" : "4",
|
||||||
|
"localizations" : {
|
||||||
|
"ar-SA" : "نقل فائق السرعة عبر الواي فاي.",
|
||||||
|
"ca" : "Transferència Wi-Fi ultraràpida.",
|
||||||
|
"cs" : "Ultra-rychlý přenos přes Wi-Fi.",
|
||||||
|
"da" : "Ultrahurtig overførsel via Wi-Fi.",
|
||||||
|
"de-DE" : "Ultraschneller Transfer über WLAN.",
|
||||||
|
"el" : "Ταχύτατη μεταφορά Wi-Fi.",
|
||||||
|
"en-AU" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"en-CA" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"en-GB" : "Ultra-fast transfer over Wi-Fi.",
|
||||||
|
"es-ES" : "Transfiere rápido por Wi-Fi.",
|
||||||
|
"es-MX" : "Transfiere rápido por Wi-Fi.",
|
||||||
|
"fi" : "Huippunopea siirto Wi-Fillä.",
|
||||||
|
"fr-CA" : "Transfert Wi-Fi ultra-rapide.",
|
||||||
|
"fr-FR" : "Transfert Wi-Fi ultra-rapide.",
|
||||||
|
"he" : "העברה מהירה במיוחד דרך Wi-Fi.",
|
||||||
|
"hi" : "Wi-Fi पर अल्ट्रा-फास्ट ट्रांसफर।",
|
||||||
|
"hr" : "Ultra-brz prijenos putem Wi-Fi-ja.",
|
||||||
|
"hu" : "Ultragyors átvitel Wi-Fin.",
|
||||||
|
"id" : "Transfer ultra-cepat melalui Wi-Fi.",
|
||||||
|
"it" : "Transfer rapido via Wi-Fi.",
|
||||||
|
"ja" : "Wi-Fi経由で超高速転送。",
|
||||||
|
"ko" : "Wi-Fi를 통한 초고속 전송.",
|
||||||
|
"ms" : "Pindahan ultra-pantas melalui Wi-Fi.",
|
||||||
|
"nl-NL" : "Ultrasnelle overdracht via Wi-Fi.",
|
||||||
|
"no" : "Ultrarask overføring over Wi-Fi.",
|
||||||
|
"pl" : "Błyskawiczny transfer przez Wi-Fi.",
|
||||||
|
"pt-BR" : "Transfira muito rápido via Wi-Fi.",
|
||||||
|
"pt-PT" : "Transfere muito rápido por Wi-Fi.",
|
||||||
|
"ro" : "Transfer ultra-rapid prin Wi-Fi.",
|
||||||
|
"ru" : "Быстрая передача Wi-Fi.",
|
||||||
|
"sk" : "Ultra-rýchly prenos cez Wi-Fi.",
|
||||||
|
"sv" : "Ultrasnabb överföring via Wi-Fi.",
|
||||||
|
"th" : "ถ่ายโอนด้วยความเร็วสูงผ่าน Wi-Fi",
|
||||||
|
"tr" : "Wi-Fi ile ultra hızlı transfer.",
|
||||||
|
"uk" : "Надшвидка передача через Wi-Fi.",
|
||||||
|
"vi" : "Chuyển siêu nhanh qua Wi-Fi.",
|
||||||
|
"zh-Hans" : "超快的 Wi-Fi 传输。",
|
||||||
|
"zh-Hant" : "超快速 Wi-Fi 傳輸。"
|
||||||
|
},
|
||||||
|
"yOffset" : 253
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sourceLocale" : "en-US",
|
||||||
|
"syncSettings" : true,
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@@ -30,11 +30,13 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
private var bytesToBeTransferred: Int64 = 0
|
private var bytesToBeTransferred: Int64 = 0
|
||||||
private var isAuthenticated = false
|
private var isAuthenticated = false
|
||||||
|
private var receiverAuthenticationAccepted = false
|
||||||
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
|
private var peerCertificate: Sharing_Nearby_PublicCertificate? = nil
|
||||||
private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata?
|
private var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata?
|
||||||
private var pairingMetadata: Sharing_Nearby_PairingMetadata?
|
private var pairingMetadata: Sharing_Nearby_PairingMetadata?
|
||||||
private var isPairingSetupRequest = false
|
private var isPairingSetupRequest = false
|
||||||
var isPlainTextTransfer = false
|
var isPlainTextTransfer = false
|
||||||
|
var isClipboardSyncTransfer = false
|
||||||
var isControlTransfer = false
|
var isControlTransfer = false
|
||||||
|
|
||||||
var wasUserRejected = false
|
var wasUserRejected = false
|
||||||
@@ -279,11 +281,22 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
if let urlStr = String(data: payload, encoding: .utf8) {
|
if let urlStr = String(data: payload, encoding: .utf8) {
|
||||||
|
|
||||||
if isPlainTextTransfer {
|
if isPlainTextTransfer {
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
ClipboardSyncCoordinator.shared.suppressNextClipboardSync(for: urlStr)
|
||||||
|
#endif
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
// macOS clipboard
|
// macOS clipboard
|
||||||
let pasteboard = NSPasteboard.general
|
let pasteboard = NSPasteboard.general
|
||||||
pasteboard.clearContents()
|
pasteboard.clearContents()
|
||||||
pasteboard.setString(urlStr, forType: .string)
|
pasteboard.setString(urlStr, forType: .string)
|
||||||
|
#if !EXTENSION
|
||||||
|
if isClipboardSyncTransfer {
|
||||||
|
ClipboardSyncNotificationPresenter.shared.present(
|
||||||
|
clipboardText: urlStr,
|
||||||
|
senderDeviceName: remoteDeviceInfo?.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
// iOS clipboard
|
// iOS clipboard
|
||||||
UIPasteboard.general.string = urlStr
|
UIPasteboard.general.string = urlStr
|
||||||
@@ -567,6 +580,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
|
private func processPairedKeyResultFrame(_ frame: Sharing_Nearby_Frame) throws {
|
||||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
|
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult") }
|
||||||
|
receiverAuthenticationAccepted = frame.v1.pairedKeyResult.status == .success
|
||||||
currentState = .receivedPairedKeyResult
|
currentState = .receivedPairedKeyResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,6 +678,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Existing trust only proves this is the same device key we trusted before.
|
||||||
|
// It does not by itself grant a new feature, such as clipboard sync.
|
||||||
let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID)
|
let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID)
|
||||||
if let trustedData {
|
if let trustedData {
|
||||||
guard let currentData = try? peerCertificate.serializedData(),
|
guard let currentData = try? peerCertificate.serializedData(),
|
||||||
@@ -671,13 +687,10 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
self.rejectTransfer(with: .reject)
|
self.rejectTransfer(with: .reject)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.notificationSyncSetupConfirmed(connection: self)
|
|
||||||
}
|
|
||||||
submitUserConsent(accepted: true, trustDevice: false)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The QR token proves the user accepted this specific pairing use case.
|
||||||
|
// Require it even for already-trusted devices before adding a new grant.
|
||||||
let matches = TrustStore.shared.confirmPendingPairingTrust(
|
let matches = TrustStore.shared.confirmPendingPairingTrust(
|
||||||
secretIdHex: peerCertificate.secretID.hex,
|
secretIdHex: peerCertificate.secretID.hex,
|
||||||
useCase: pairingUseCase,
|
useCase: pairingUseCase,
|
||||||
@@ -689,9 +702,19 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
TrustStore.shared.addTrusted(certificate: peerCertificate, device: device)
|
// Preserve the existing trusted certificate when present; otherwise store
|
||||||
|
// the newly paired device and attach the confirmed use-case grant.
|
||||||
|
if trustedData != nil {
|
||||||
|
TrustStore.shared.grantUseCase(pairingUseCase, for: peerCertificate.secretID.hex)
|
||||||
|
} else {
|
||||||
|
TrustStore.shared.addTrusted(
|
||||||
|
certificate: peerCertificate,
|
||||||
|
device: device,
|
||||||
|
grantedUseCases: [pairingUseCase]
|
||||||
|
)
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.notificationSyncSetupConfirmed(connection: self)
|
self.delegate?.pairingSetupConfirmed(connection: self)
|
||||||
}
|
}
|
||||||
submitUserConsent(accepted: true, trustDevice: false)
|
submitUserConsent(accepted: true, trustDevice: false)
|
||||||
return
|
return
|
||||||
@@ -724,8 +747,32 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
if let textMetadata = frame.v1.introduction.textMetadata.first {
|
if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||||
let isURL = textMetadata.type == .url
|
let isURL = textMetadata.type == .url
|
||||||
textPayloadID = textMetadata.payloadID
|
textPayloadID = textMetadata.payloadID
|
||||||
|
let isRemoteCopy = frame.v1.introduction.hasUseCase
|
||||||
|
&& frame.v1.introduction.useCase == .remoteCopy
|
||||||
|
if isRemoteCopy && !isURL {
|
||||||
|
let peerFingerprint = peerCertificate?.secretID.hex
|
||||||
|
let isClipboardSyncAllowed = isAuthenticated
|
||||||
|
&& receiverAuthenticationAccepted
|
||||||
|
&& TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: peerFingerprint)
|
||||||
|
guard isClipboardSyncAllowed else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Rejecting clipboard sync transfer because the sender is not trusted for clipboard sync.")
|
||||||
|
self.rejectTransfer(with: .reject, markAsUserRejected: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isClipboardSyncTransfer = true
|
||||||
|
} else {
|
||||||
|
isClipboardSyncTransfer = false
|
||||||
|
}
|
||||||
|
|
||||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isURL ? .url : .text, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
let metadata = TransferMetadata(
|
||||||
|
files: [],
|
||||||
|
id: id,
|
||||||
|
pinCode: pinCode,
|
||||||
|
textDescription: textMetadata.textTitle,
|
||||||
|
transferType: isURL ? .url : .text,
|
||||||
|
allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil,
|
||||||
|
isClipboardSync: isClipboardSyncTransfer
|
||||||
|
)
|
||||||
|
|
||||||
if !isURL {
|
if !isURL {
|
||||||
isPlainTextTransfer = true
|
isPlainTextTransfer = true
|
||||||
@@ -774,7 +821,6 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
|
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
|
||||||
let secretIdHex = peerCertificate.secretID.hex
|
let secretIdHex = peerCertificate.secretID.hex
|
||||||
if accepted {
|
if accepted {
|
||||||
if TrustStore.shared.findTrustedKey(for: peerCertificate.secretID) == nil {
|
|
||||||
if let token = pairingToken {
|
if let token = pairingToken {
|
||||||
TrustStore.shared.registerPendingPairingTrust(
|
TrustStore.shared.registerPendingPairingTrust(
|
||||||
secretIdHex: secretIdHex,
|
secretIdHex: secretIdHex,
|
||||||
@@ -785,7 +831,6 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
|
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
|
||||||
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
}
|
}
|
||||||
@@ -873,7 +918,9 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
switch useCase {
|
switch useCase {
|
||||||
case .notificationSync:
|
case .notificationSync:
|
||||||
return .notificationSync
|
return .notificationSync
|
||||||
case .clipboardSync, .none:
|
case .clipboardSync:
|
||||||
|
return .clipboardSync
|
||||||
|
case .none:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
//
|
||||||
|
// NearbyBonjourServiceResolver.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 19.05.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
struct NearbyResolvedBonjourEndpoint {
|
||||||
|
let endpoint: NWEndpoint
|
||||||
|
/// Numeric host string; IPv6 link-local hosts may include a `%scopeID` suffix for NWEndpoint routing.
|
||||||
|
let host: String
|
||||||
|
let port: UInt16
|
||||||
|
let interface: NWInterface?
|
||||||
|
let description: String
|
||||||
|
let isIPv4: Bool
|
||||||
|
let isLinkLocal: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class NearbyBonjourServiceResolver: NSObject, NetServiceDelegate {
|
||||||
|
private static let defaultWatchdogGracePeriod: TimeInterval = 0.5
|
||||||
|
|
||||||
|
private let service: NetService
|
||||||
|
private let resolveTimeout: TimeInterval
|
||||||
|
private let watchdogGracePeriod: TimeInterval
|
||||||
|
private let interface: NWInterface?
|
||||||
|
private var timeoutWorkItem: DispatchWorkItem?
|
||||||
|
private var completion: (([NearbyResolvedBonjourEndpoint]) -> Void)?
|
||||||
|
private var finished = false
|
||||||
|
|
||||||
|
init?(
|
||||||
|
result: NWBrowser.Result,
|
||||||
|
resolveTimeout: TimeInterval,
|
||||||
|
watchdogGracePeriod: TimeInterval = defaultWatchdogGracePeriod,
|
||||||
|
completion: @escaping ([NearbyResolvedBonjourEndpoint]) -> Void
|
||||||
|
) {
|
||||||
|
guard let components = Self.serviceEndpointComponents(from: result) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.service = NetService(
|
||||||
|
domain: Self.normalizedServiceDomain(components.domain),
|
||||||
|
type: Self.normalizedServiceType(components.type),
|
||||||
|
name: components.name
|
||||||
|
)
|
||||||
|
self.resolveTimeout = resolveTimeout
|
||||||
|
self.watchdogGracePeriod = watchdogGracePeriod
|
||||||
|
self.interface = components.interface
|
||||||
|
self.completion = completion
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func serviceEndpointComponents(
|
||||||
|
from result: NWBrowser.Result
|
||||||
|
) -> (name: String, type: String, domain: String, interface: NWInterface?)? {
|
||||||
|
guard case let NWEndpoint.service(name: name, type: type, domain: domain, interface: interface) = result.endpoint else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return (name: name, type: type, domain: domain, interface: interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
service.delegate = self
|
||||||
|
service.resolve(withTimeout: resolveTimeout)
|
||||||
|
|
||||||
|
let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
self?.finish(endpoints: [])
|
||||||
|
}
|
||||||
|
self.timeoutWorkItem = timeoutWorkItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeout + watchdogGracePeriod, execute: timeoutWorkItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
finish(endpoints: [], notify: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func netServiceDidResolveAddress(_ sender: NetService) {
|
||||||
|
finish(endpoints: Self.resolvedEndpoints(from: sender, interface: interface))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
|
||||||
|
finish(endpoints: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func finish(endpoints: [NearbyResolvedBonjourEndpoint], notify: Bool = true) {
|
||||||
|
guard !finished else { return }
|
||||||
|
finished = true
|
||||||
|
timeoutWorkItem?.cancel()
|
||||||
|
service.stop()
|
||||||
|
service.delegate = nil
|
||||||
|
|
||||||
|
let completion = completion
|
||||||
|
self.completion = nil
|
||||||
|
|
||||||
|
if notify {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion?(endpoints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func resolvedEndpoints(
|
||||||
|
from service: NetService,
|
||||||
|
interface: NWInterface?
|
||||||
|
) -> [NearbyResolvedBonjourEndpoint] {
|
||||||
|
let endpoints = (service.addresses ?? [])
|
||||||
|
.compactMap { resolvedEndpoint(from: $0, interface: interface) }
|
||||||
|
|
||||||
|
var seenDescriptions = Set<String>()
|
||||||
|
let uniqueEndpoints = endpoints.filter { endpoint in
|
||||||
|
seenDescriptions.insert(endpoint.description).inserted
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueEndpoints.sorted { lhs, rhs in
|
||||||
|
if lhs.isIPv4 != rhs.isIPv4 {
|
||||||
|
return lhs.isIPv4
|
||||||
|
}
|
||||||
|
if lhs.isLinkLocal != rhs.isLinkLocal {
|
||||||
|
return !lhs.isLinkLocal
|
||||||
|
}
|
||||||
|
return lhs.description < rhs.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func resolvedEndpoint(
|
||||||
|
from addressData: Data,
|
||||||
|
interface: NWInterface?
|
||||||
|
) -> NearbyResolvedBonjourEndpoint? {
|
||||||
|
addressData.withUnsafeBytes { rawBuffer in
|
||||||
|
guard let baseAddress = rawBuffer.baseAddress else { return nil }
|
||||||
|
|
||||||
|
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
|
||||||
|
let addressFamily = Int32(sockaddrPointer.pointee.sa_family)
|
||||||
|
|
||||||
|
guard addressFamily == AF_INET || addressFamily == AF_INET6 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
var portBuffer = [CChar](repeating: 0, count: Int(NI_MAXSERV))
|
||||||
|
let result = getnameinfo(
|
||||||
|
sockaddrPointer,
|
||||||
|
socklen_t(addressData.count),
|
||||||
|
&hostBuffer,
|
||||||
|
socklen_t(hostBuffer.count),
|
||||||
|
&portBuffer,
|
||||||
|
socklen_t(portBuffer.count),
|
||||||
|
NI_NUMERICHOST | NI_NUMERICSERV
|
||||||
|
)
|
||||||
|
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
|
||||||
|
var host = String(cString: hostBuffer)
|
||||||
|
if addressFamily == AF_INET6 {
|
||||||
|
host = scopedIPv6Host(host, addressData: addressData)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let portRawValue = UInt16(String(cString: portBuffer)),
|
||||||
|
let port = NWEndpoint.Port(rawValue: portRawValue) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(host), port: port)
|
||||||
|
let description: String
|
||||||
|
if addressFamily == AF_INET6 {
|
||||||
|
description = "[\(host)]:\(portRawValue)"
|
||||||
|
} else {
|
||||||
|
description = "\(host):\(portRawValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let isIPv6LinkLocal = addressFamily == AF_INET6 && host.lowercased().hasPrefix("fe80:")
|
||||||
|
|
||||||
|
return NearbyResolvedBonjourEndpoint(
|
||||||
|
endpoint: endpoint,
|
||||||
|
host: host,
|
||||||
|
port: portRawValue,
|
||||||
|
interface: isIPv6LinkLocal ? interface : nil,
|
||||||
|
description: description,
|
||||||
|
isIPv4: addressFamily == AF_INET,
|
||||||
|
isLinkLocal: isIPv6LinkLocal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func scopedIPv6Host(
|
||||||
|
_ host: String,
|
||||||
|
addressData: Data
|
||||||
|
) -> String {
|
||||||
|
guard !host.contains("%"),
|
||||||
|
host.lowercased().hasPrefix("fe80:") else {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let scopeID = ipv6ScopeID(from: addressData), scopeID != 0 else {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(host)%\(scopeID)"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func ipv6ScopeID(from addressData: Data) -> UInt32? {
|
||||||
|
addressData.withUnsafeBytes { rawBuffer in
|
||||||
|
guard let baseAddress = rawBuffer.baseAddress else { return nil }
|
||||||
|
let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self)
|
||||||
|
guard Int32(sockaddrPointer.pointee.sa_family) == AF_INET6 else { return nil }
|
||||||
|
return baseAddress.assumingMemoryBound(to: sockaddr_in6.self).pointee.sin6_scope_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func normalizedServiceDomain(_ domain: String) -> String {
|
||||||
|
let trimmedDomain = domain.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedDomain.isEmpty else { return "local." }
|
||||||
|
return trimmedDomain.hasSuffix(".") ? trimmedDomain : "\(trimmedDomain)."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func normalizedServiceType(_ type: String) -> String {
|
||||||
|
let trimmedType = type.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return trimmedType.hasSuffix(".") ? trimmedType : "\(trimmedType)."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ class NearbyConnection {
|
|||||||
private var dataTransferInactivityTimer: DispatchSourceTimer?
|
private var dataTransferInactivityTimer: DispatchSourceTimer?
|
||||||
|
|
||||||
private let ackInterval: TimeInterval = 5
|
private let ackInterval: TimeInterval = 5
|
||||||
private let ackTimeoutInterval: TimeInterval = 30
|
private let ackTimeoutInterval: TimeInterval = 60
|
||||||
private var lastKeepAliveFrameSent = Date()
|
private var lastKeepAliveFrameSent = Date()
|
||||||
|
|
||||||
// UKEY2-related state
|
// UKEY2-related state
|
||||||
@@ -424,14 +424,29 @@ class NearbyConnection {
|
|||||||
let chunk = payloadTransfer.payloadChunk
|
let chunk = payloadTransfer.payloadChunk
|
||||||
|
|
||||||
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
|
guard header.hasType, header.hasID else { throw NearbyError.requiredFieldMissing("payloadHeader.type|id") }
|
||||||
guard payloadTransfer.hasPayloadChunk, chunk.hasOffset, chunk.hasFlags else {
|
|
||||||
|
|
||||||
if payloadTransfer.controlMessage.event == .payloadCanceled {
|
if payloadTransfer.hasPayloadChunk {
|
||||||
|
guard chunk.hasOffset, chunk.hasFlags else {
|
||||||
|
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
|
||||||
|
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
||||||
|
}
|
||||||
|
} else if payloadTransfer.hasControlMessage || payloadTransfer.packetType == .control {
|
||||||
|
switch payloadTransfer.controlMessage.event {
|
||||||
|
case .payloadCanceled:
|
||||||
log("[NearbyConnection \(self.id)] Cancel control frame received.")
|
log("[NearbyConnection \(self.id)] Cancel control frame received.")
|
||||||
throw NearbyError.canceled(reason: .userCanceled)
|
throw NearbyError.canceled(reason: .userCanceled)
|
||||||
|
case .payloadReceivedAck:
|
||||||
|
// Legacy peers can send ACKs as control events instead of packetType=.payloadAck.
|
||||||
|
return
|
||||||
|
case .payloadError:
|
||||||
|
throw NearbyError.protocolError("Peer reported payload error")
|
||||||
|
case .unknownEventType:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
} else if payloadTransfer.packetType == .payloadAck {
|
||||||
log("[NearbyConnection \(self.id)] Payload transfer chunk is missing offset or flags. Frame is \(payloadTransfer.debugDescription)")
|
return
|
||||||
|
} else {
|
||||||
|
log("[NearbyConnection \(self.id)] Payload transfer frame missing supported payload content. Frame is \(payloadTransfer.debugDescription)")
|
||||||
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
throw NearbyError.requiredFieldMissing("payloadChunk.offset|flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +499,9 @@ class NearbyConnection {
|
|||||||
// Older peers can omit the keepAlive payload; default to ACK reply.
|
// Older peers can omit the keepAlive payload; default to ACK reply.
|
||||||
sendKeepAlive(ack: true)
|
sendKeepAlive(ack: true)
|
||||||
}
|
}
|
||||||
|
} else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .disconnection = offlineFrame.v1.type {
|
||||||
|
log("[NearbyConnection \(self.id)] Received disconnection frame.")
|
||||||
|
disconnect()
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry {
|
if offlineFrame.hasV1, offlineFrame.v1.hasType, offlineFrame.v1.type == .bandwidthUpgradeRetry {
|
||||||
|
|||||||
@@ -33,9 +33,14 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
private var outboundAppDelegates: [OutboundAppDelegate] = []
|
||||||
private var inboundAppDelegates: [InboundAppDelegate] = []
|
private var inboundAppDelegates: [InboundAppDelegate] = []
|
||||||
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
private var outgoingTransfers: [String: OutgoingTransferInfo] = [:]
|
||||||
|
private var pendingOutgoingTransferPreparations: [String: UUID] = [:]
|
||||||
|
private var outgoingServiceResolvers: [String: NearbyBonjourServiceResolver] = [:]
|
||||||
|
private let pendingNotificationSyncTrustQueue = DispatchQueue(label: "NearbyConnectionManager.pendingNotificationSyncTrust")
|
||||||
|
private var pendingNotificationSyncTrust: [String: PendingReceiverTrust] = [:]
|
||||||
private var startedDeviceDiscovery = false
|
private var startedDeviceDiscovery = false
|
||||||
private var startedAdvertising = false { didSet { informAboutStatus() }}
|
private var startedAdvertising = false { didSet { informAboutStatus() }}
|
||||||
private var browsers: [NWBrowser] = []
|
private var browsers: [NWBrowser] = []
|
||||||
|
private let deviceNameResolver = NearbyDeviceNameResolver()
|
||||||
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
|
private let serviceTypes = ["_FC9F5ED42C8A._tcp."]
|
||||||
private var qrCodePrivateKey: ECPrivateKey?
|
private var qrCodePrivateKey: ECPrivateKey?
|
||||||
private var qrCodeAdvertisingToken: Data?
|
private var qrCodeAdvertisingToken: Data?
|
||||||
@@ -46,9 +51,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
|
private static let customDeviceNameKey = "com.leonboettger.quickdrop.deviceName"
|
||||||
private var defaultPort: NWEndpoint.Port = 50362
|
private var defaultPort: NWEndpoint.Port = 50362
|
||||||
private let txtKeyEndpointInfo = "n"
|
private let txtKeyEndpointInfo = "n"
|
||||||
private let txtKeyNotificationSyncCapabilities = "qd_caps"
|
private let txtQuickDropCapabilities = "qd_caps"
|
||||||
private let txtKeyNotificationSyncKeyFingerprint = "qd_kid"
|
private let txtQuickDropDeviceKeyFingerprint = "qd_kid"
|
||||||
private let notificationSyncCapabilityToken = "nsync1"
|
private let notificationSyncCapabilityToken = "nsync1"
|
||||||
|
private let clipboardReceiveCapabilityToken = "cliprecv1"
|
||||||
private let unnamedAndroidFallbackDeviceName = "AndroidDevice".localized()
|
private let unnamedAndroidFallbackDeviceName = "AndroidDevice".localized()
|
||||||
|
|
||||||
|
|
||||||
@@ -315,11 +321,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
var txtRecord: [String: Data] = [
|
var txtRecord: [String: Data] = [
|
||||||
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
|
txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!,
|
||||||
]
|
]
|
||||||
#if os(macOS)
|
|
||||||
txtRecord[txtKeyNotificationSyncCapabilities] = Data(notificationSyncCapabilityToken.utf8)
|
|
||||||
if let keyFingerprint = notificationSyncKeyFingerprintHex() {
|
if let keyFingerprint = notificationSyncKeyFingerprintHex() {
|
||||||
txtRecord[txtKeyNotificationSyncKeyFingerprint] = Data(keyFingerprint.utf8)
|
txtRecord[txtQuickDropDeviceKeyFingerprint] = Data(keyFingerprint.utf8)
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
let capabilityTokens = [
|
||||||
|
notificationSyncCapabilityToken,
|
||||||
|
clipboardReceiveCapabilityToken,
|
||||||
|
]
|
||||||
|
txtRecord[txtQuickDropCapabilities] = Data(capabilityTokens.joined(separator: ",").utf8)
|
||||||
#endif
|
#endif
|
||||||
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
|
service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord))
|
||||||
service.publish()
|
service.publish()
|
||||||
@@ -370,14 +380,21 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
if !connection.wasUserRejected {
|
if !connection.wasUserRejected {
|
||||||
inboundAppDelegates.forEach { delegate in
|
inboundAppDelegates.forEach { delegate in
|
||||||
delegate.connectionWasTerminated(connectionID: connection.id, from: connection.remoteDeviceInfo, savedFiles: savedFiles, wasPlainTextTransfer: connection.isPlainTextTransfer, error: error)
|
delegate.connectionWasTerminated(
|
||||||
|
connectionID: connection.id,
|
||||||
|
from: connection.remoteDeviceInfo,
|
||||||
|
savedFiles: savedFiles,
|
||||||
|
wasPlainTextTransfer: connection.isPlainTextTransfer,
|
||||||
|
wasClipboardSync: connection.isClipboardSyncTransfer,
|
||||||
|
error: error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationSyncSetupConfirmed(connection _: InboundNearbyConnection) {
|
func pairingSetupConfirmed(connection _: InboundNearbyConnection) {
|
||||||
inboundAppDelegates.forEach { delegate in
|
inboundAppDelegates.forEach { delegate in
|
||||||
delegate.notificationSyncSetupConfirmed()
|
delegate.pairingSetupConfirmed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +446,8 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
self.addFoundDevice(service: res)
|
self.addFoundDevice(service: res)
|
||||||
case let .removed(res):
|
case let .removed(res):
|
||||||
self.removeFoundDevice(service: res)
|
self.removeFoundDevice(service: res)
|
||||||
|
case let .changed(_, new, _):
|
||||||
|
self.addFoundDevice(service: new)
|
||||||
default:
|
default:
|
||||||
log("[NearbyConnectionManager] Ignoring change \(change)")
|
log("[NearbyConnectionManager] Ignoring change \(change)")
|
||||||
}
|
}
|
||||||
@@ -473,7 +492,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
browsers.removeAll()
|
browsers.removeAll()
|
||||||
startedDeviceDiscovery = false
|
startedDeviceDiscovery = false
|
||||||
|
deviceNameResolver.cancelAll()
|
||||||
|
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
|
|
||||||
@@ -486,6 +505,16 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var isDeviceDiscoveryActive: Bool {
|
||||||
|
startedDeviceDiscovery
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func discoveredDevices() -> [RemoteDeviceInfo] {
|
||||||
|
foundServices.values.compactMap(\.device)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
public func addOutboundAppDelegate(_ delegate: OutboundAppDelegate) {
|
||||||
outboundAppDelegates.append(delegate)
|
outboundAppDelegates.append(delegate)
|
||||||
for service in foundServices.values {
|
for service in foundServices.values {
|
||||||
@@ -510,10 +539,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
|
|
||||||
|
|
||||||
public func cancelTransfer(id: String) {
|
public func cancelTransfer(id: String) {
|
||||||
|
|
||||||
if let transfer = incomingConnections[id] {
|
if let transfer = incomingConnections[id] {
|
||||||
transfer.cancel()
|
transfer.cancel()
|
||||||
}
|
}
|
||||||
|
pendingOutgoingTransferPreparations.removeValue(forKey: id)
|
||||||
|
outgoingServiceResolvers.removeValue(forKey: id)?.cancel()
|
||||||
if let info = outgoingTransfers[id] {
|
if let info = outgoingTransfers[id] {
|
||||||
info.connection.cancel()
|
info.connection.cancel()
|
||||||
}
|
}
|
||||||
@@ -552,14 +582,57 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
|
guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return }
|
||||||
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
|
guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return }
|
||||||
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
|
guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return }
|
||||||
var endpointInfo = EndpointInfo(data: endpointInfoData)
|
let parsedEndpointInfo = EndpointInfo(data: endpointInfoData)
|
||||||
|
let keyFingerprint = normalizedKeyFingerprint(
|
||||||
|
txtRecord.dictionary[txtQuickDropDeviceKeyFingerprint]
|
||||||
|
)
|
||||||
|
let capabilityTokens = Set(
|
||||||
|
txtRecord.dictionary[txtQuickDropCapabilities]?
|
||||||
|
.split(separator: ",")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty } ?? []
|
||||||
|
)
|
||||||
|
let supportsNotificationSync = capabilityTokens.contains(notificationSyncCapabilityToken)
|
||||||
|
let supportsClipboardReceive = capabilityTokens.contains(clipboardReceiveCapabilityToken)
|
||||||
|
var endpointInfo = parsedEndpointInfo
|
||||||
?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown)
|
?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown)
|
||||||
|
let isQuickDropPeer = keyFingerprint != nil
|
||||||
let cleanedName = endpointInfo.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let needsDeviceNameLookup = deviceNameResolver.needsLookup(for: parsedEndpointInfo)
|
||||||
if cleanedName?.isEmpty != false {
|
if needsDeviceNameLookup {
|
||||||
endpointInfo.name = unnamedAndroidFallbackDeviceName
|
endpointInfo.name = unnamedAndroidFallbackDeviceName
|
||||||
}
|
}
|
||||||
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
let deviceInfo = addFoundDevice(
|
||||||
|
foundService: &foundService,
|
||||||
|
endpointInfo: endpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
|
|
||||||
|
if needsDeviceNameLookup {
|
||||||
|
deviceNameResolver.resolveName(for: service, endpointID: endpointID) { [weak self] deviceName in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.foundServices[endpointID]?.device?.name == self.unnamedAndroidFallbackDeviceName else { return }
|
||||||
|
|
||||||
|
var resolvedEndpointInfo = endpointInfo
|
||||||
|
resolvedEndpointInfo.name = deviceName
|
||||||
|
var resolvedFoundService = FoundServiceInfo(service: service)
|
||||||
|
_ = self.addFoundDevice(
|
||||||
|
foundService: &resolvedFoundService,
|
||||||
|
endpointInfo: resolvedEndpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deviceNameResolver.cancelLookup(for: endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
|
if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey {
|
||||||
|
|
||||||
@@ -576,7 +649,15 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
|
let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken)
|
||||||
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
guard let name = String.init(data: decryptedName, encoding: .utf8) else { return }
|
||||||
endpointInfo.name = name
|
endpointInfo.name = name
|
||||||
let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID)
|
let deviceInfo = addFoundDevice(
|
||||||
|
foundService: &foundService,
|
||||||
|
endpointInfo: endpointInfo,
|
||||||
|
endpointID: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.startTransferWithQrCode(device: deviceInfo)
|
delegate.startTransferWithQrCode(device: deviceInfo)
|
||||||
}
|
}
|
||||||
@@ -588,9 +669,24 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo {
|
private func addFoundDevice(
|
||||||
|
foundService: inout FoundServiceInfo,
|
||||||
|
endpointInfo: EndpointInfo,
|
||||||
|
endpointID: String,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) -> RemoteDeviceInfo {
|
||||||
let hadPreviousDevice = foundServices[endpointID]?.device != nil
|
let hadPreviousDevice = foundServices[endpointID]?.device != nil
|
||||||
let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID)
|
let deviceInfo = RemoteDeviceInfo(
|
||||||
|
info: endpointInfo,
|
||||||
|
id: endpointID,
|
||||||
|
isQuickDropPeer: isQuickDropPeer,
|
||||||
|
keyFingerprint: keyFingerprint,
|
||||||
|
supportsNotificationSync: supportsNotificationSync,
|
||||||
|
supportsClipboardReceive: supportsClipboardReceive
|
||||||
|
)
|
||||||
foundService.device = deviceInfo
|
foundService.device = deviceInfo
|
||||||
foundServices[endpointID] = foundService
|
foundServices[endpointID] = foundService
|
||||||
|
|
||||||
@@ -608,9 +704,28 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func resolveDiscoveredDeviceIdForKeyFingerprint(_ keyFingerprint: String) -> String? {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(keyFingerprint) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundServices.values
|
||||||
|
.first(where: { $0.device?.keyFingerprint == normalizedFingerprint })?
|
||||||
|
.device?
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func removeFoundDevice(service: NWBrowser.Result) {
|
private func removeFoundDevice(service: NWBrowser.Result) {
|
||||||
guard let endpointID = endpointID(for: service) else { return }
|
guard let endpointID = endpointID(for: service) else { return }
|
||||||
guard let _ = foundServices.removeValue(forKey: endpointID) else { return }
|
deviceNameResolver.cancelLookup(for: endpointID)
|
||||||
|
guard let currentService = foundServices[endpointID] else { return }
|
||||||
|
// Ignore delayed removals for an older Bonjour result after a changed result
|
||||||
|
// has already replaced the service for this endpoint ID.
|
||||||
|
guard serviceIdentity(currentService.service) == serviceIdentity(service) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
foundServices.removeValue(forKey: endpointID)
|
||||||
for delegate in outboundAppDelegates {
|
for delegate in outboundAppDelegates {
|
||||||
delegate.removeDevice(id: endpointID)
|
delegate.removeDevice(id: endpointID)
|
||||||
}
|
}
|
||||||
@@ -657,32 +772,132 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func startOutgoingTransfer(deviceID: String, delegate: OutboundAppDelegate, urls: [URL], textToSend: String?) {
|
@discardableResult
|
||||||
|
public func startOutgoingTransfer(
|
||||||
|
deviceID: String,
|
||||||
|
delegate: OutboundAppDelegate,
|
||||||
|
urls: [URL],
|
||||||
|
textToSend: String?,
|
||||||
|
preparationFinished: (() -> Void)? = nil,
|
||||||
|
isClipboardSyncTransfer: Bool = false,
|
||||||
|
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
|
) -> Bool {
|
||||||
log("Starting outgoing transfer to \(deviceID)")
|
log("Starting outgoing transfer to \(deviceID)")
|
||||||
guard let info = foundServices[deviceID] else { return }
|
guard foundServices[deviceID] != nil else {
|
||||||
|
let error = NearbyError.protocolError("TargetDeviceNoLongerNearby".localized())
|
||||||
|
delegate.connectionFailed(error: error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let preparationID = UUID()
|
||||||
|
pendingOutgoingTransferPreparations[deviceID] = preparationID
|
||||||
|
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async { [weak self, weak delegate] in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let localUrls = try saveFilesToTemp(urls: urls)
|
let localUrls = try self.saveFilesToTemp(urls: urls)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let info = self.foundServices[deviceID],
|
||||||
|
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let delegate else {
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preparationFinished?()
|
||||||
|
|
||||||
|
self.resolveOutgoingEndpoint(for: info.service, deviceID: deviceID) { endpoint, requiredInterface in
|
||||||
|
guard let info = self.foundServices[deviceID],
|
||||||
|
self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
|
||||||
let tcp = NWProtocolTCP.Options()
|
let tcp = NWProtocolTCP.Options()
|
||||||
tcp.noDelay = true
|
tcp.noDelay = true
|
||||||
let nwconn = NWConnection(to: info.service.endpoint, using: NWParameters(tls: .none, tcp: tcp))
|
let parameters = NWParameters(tls: .none, tcp: tcp)
|
||||||
|
parameters.requiredInterface = requiredInterface
|
||||||
|
let nwconn = NWConnection(to: endpoint, using: parameters)
|
||||||
|
|
||||||
let conn = OutboundNearbyConnection(connection: nwconn, id: deviceID, urlsToSend: localUrls, textToSend: textToSend)
|
let conn = OutboundNearbyConnection(
|
||||||
|
connection: nwconn,
|
||||||
|
id: deviceID,
|
||||||
|
urlsToSend: localUrls,
|
||||||
|
textToSend: textToSend,
|
||||||
|
isClipboardSyncTransfer: isClipboardSyncTransfer,
|
||||||
|
receiverAuthenticationPolicy: receiverAuthenticationPolicy
|
||||||
|
)
|
||||||
|
conn.remoteDeviceInfo = info.device
|
||||||
conn.delegate = self
|
conn.delegate = self
|
||||||
conn.qrCodePrivateKey = qrCodePrivateKey
|
conn.qrCodePrivateKey = self.qrCodePrivateKey
|
||||||
|
|
||||||
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
let transfer = OutgoingTransferInfo(service: info.service, device: info.device!, connection: conn, delegate: delegate)
|
||||||
outgoingTransfers[deviceID] = transfer
|
self.outgoingTransfers[deviceID] = transfer
|
||||||
conn.start()
|
conn.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
log("[NearbyConnectionManager] Error storing URLs: \(error)")
|
||||||
outboundAppDelegates.forEach { delegate in
|
DispatchQueue.main.async {
|
||||||
delegate.connectionFailed(error: error)
|
guard self.pendingOutgoingTransferPreparations[deviceID] == preparationID else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pendingOutgoingTransferPreparations.removeValue(forKey: deviceID)
|
||||||
|
delegate?.connectionFailed(error: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func resolveOutgoingEndpoint(
|
||||||
|
for service: NWBrowser.Result,
|
||||||
|
deviceID: String,
|
||||||
|
completion: @escaping (NWEndpoint, NWInterface?) -> Void
|
||||||
|
) {
|
||||||
|
guard let resolver = NearbyBonjourServiceResolver(result: service, resolveTimeout: 2.5, completion: { [weak self] endpoints in
|
||||||
|
guard let self else { return }
|
||||||
|
self.outgoingServiceResolvers.removeValue(forKey: deviceID)
|
||||||
|
|
||||||
|
if let endpoint = endpoints.first {
|
||||||
|
completion(endpoint.endpoint, endpoint.interface)
|
||||||
|
} else {
|
||||||
|
log("[NearbyConnectionManager] Could not resolve outgoing service for \(deviceID); falling back to Bonjour endpoint \(service.endpoint)")
|
||||||
|
completion(service.endpoint, nil)
|
||||||
|
}
|
||||||
|
}) else {
|
||||||
|
log("[NearbyConnectionManager] Cannot create resolver for outgoing service; falling back to Bonjour endpoint")
|
||||||
|
completion(service.endpoint, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outgoingServiceResolvers.removeValue(forKey: deviceID)?.cancel()
|
||||||
|
outgoingServiceResolvers[deviceID] = resolver
|
||||||
|
resolver.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
|
||||||
|
let normalized = keyFingerprint?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
return normalized?.isEmpty == false ? normalized : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func serviceIdentity(_ service: NWBrowser.Result) -> String {
|
||||||
|
String(describing: service.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func connectionWasEstablished(connection: OutboundNearbyConnection) {
|
func connectionWasEstablished(connection: OutboundNearbyConnection) {
|
||||||
guard let transfer = outgoingTransfers[connection.id] else { return }
|
guard let transfer = outgoingTransfers[connection.id] else { return }
|
||||||
@@ -726,6 +941,78 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func receiverAuthenticationPending(
|
||||||
|
connection _: OutboundNearbyConnection,
|
||||||
|
certificateData: Data,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
fingerprint: String
|
||||||
|
) {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
|
||||||
|
log("[NearbyConnectionManager] Receiver auth pending: fingerprint=\(normalizedFingerprint) device=\(device.name ?? "unknown") (\(device.id ?? "no-id"))")
|
||||||
|
pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust[normalizedFingerprint] = PendingReceiverTrust(
|
||||||
|
certificateData: certificateData,
|
||||||
|
device: device
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func commitNotificationSyncReceiverTrust(_ fingerprint: String) -> Bool {
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let pending = pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending == nil {
|
||||||
|
let alreadyTrusted = TrustStore.shared.trustedCertificates.keys.contains(normalizedFingerprint)
|
||||||
|
if alreadyTrusted {
|
||||||
|
log("[NearbyConnectionManager] Commit trust skipped: already trusted for \(normalizedFingerprint)")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: no pending entry for \(normalizedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pending,
|
||||||
|
let certificate = try? Sharing_Nearby_PublicCertificate(serializedBytes: pending.certificateData) else {
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: could not parse certificate for \(normalizedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let derivedFingerprint = certificate.secretID.hex.lowercased()
|
||||||
|
guard derivedFingerprint == normalizedFingerprint else {
|
||||||
|
log("[NearbyConnectionManager] Commit trust failed: fingerprint mismatch pending=\(normalizedFingerprint) derived=\(derivedFingerprint)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[NearbyConnectionManager] Commit trust succeeded for fingerprint=\(normalizedFingerprint)")
|
||||||
|
TrustStore.shared.addTrusted(certificate: certificate, device: pending.device)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func clearPendingNotificationSyncReceiverTrust(_ fingerprint: String? = nil) {
|
||||||
|
if fingerprint == nil {
|
||||||
|
pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeAll()
|
||||||
|
}
|
||||||
|
log("[NearbyConnectionManager] Cleared all pending receiver trust entries")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let normalizedFingerprint = normalizedKeyFingerprint(fingerprint) else { return }
|
||||||
|
_ = pendingNotificationSyncTrustQueue.sync {
|
||||||
|
pendingNotificationSyncTrust.removeValue(forKey: normalizedFingerprint)
|
||||||
|
}
|
||||||
|
log("[NearbyConnectionManager] Cleared pending receiver trust for \(normalizedFingerprint)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public func getActiveConnectionsCount() -> Int {
|
public func getActiveConnectionsCount() -> Int {
|
||||||
return incomingConnections.count + outgoingTransfers.count
|
return incomingConnections.count + outgoingTransfers.count
|
||||||
}
|
}
|
||||||
@@ -862,4 +1149,10 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
|
|||||||
let connection: OutboundNearbyConnection
|
let connection: OutboundNearbyConnection
|
||||||
let delegate: OutboundAppDelegate
|
let delegate: OutboundAppDelegate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private struct PendingReceiverTrust {
|
||||||
|
let certificateData: Data
|
||||||
|
let device: RemoteDeviceInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// NearbyDeviceNameResolver.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 25.04.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
final class NearbyDeviceNameResolver {
|
||||||
|
private static let hostnameLookupQueue = DispatchQueue(
|
||||||
|
label: "com.leonboettger.quickdrop.deviceNameHostnameLookup",
|
||||||
|
qos: .utility
|
||||||
|
)
|
||||||
|
|
||||||
|
private var hostnameResolvers: [UUID: NearbyBonjourServiceResolver] = [:]
|
||||||
|
private var lookupTokens: [String: UUID] = [:]
|
||||||
|
|
||||||
|
func needsLookup(for endpointInfo: EndpointInfo?) -> Bool {
|
||||||
|
let cleanedName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return cleanedName?.isEmpty != false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func resolveName(for service: NWBrowser.Result, endpointID: String, completion: @escaping (String) -> Void) {
|
||||||
|
let lookupToken = UUID()
|
||||||
|
lookupTokens[endpointID] = lookupToken
|
||||||
|
|
||||||
|
resolveAdvertisedHostname(for: service) { [weak self] hostname in
|
||||||
|
guard let self else { return }
|
||||||
|
guard self.lookupTokens[endpointID] == lookupToken else { return }
|
||||||
|
self.lookupTokens.removeValue(forKey: endpointID)
|
||||||
|
guard let hostname else { return }
|
||||||
|
completion(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancelLookup(for endpointID: String) {
|
||||||
|
lookupTokens.removeValue(forKey: endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancelAll() {
|
||||||
|
lookupTokens.removeAll()
|
||||||
|
hostnameResolvers.values.forEach { $0.cancel() }
|
||||||
|
hostnameResolvers.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func resolveAdvertisedHostname(for service: NWBrowser.Result, completion: @escaping (String?) -> Void) {
|
||||||
|
let resolverID = UUID()
|
||||||
|
guard let resolver = NearbyBonjourServiceResolver(
|
||||||
|
result: service,
|
||||||
|
resolveTimeout: 0.5,
|
||||||
|
watchdogGracePeriod: 1.0,
|
||||||
|
completion: { [weak self] endpoints in
|
||||||
|
self?.hostnameResolvers.removeValue(forKey: resolverID)
|
||||||
|
guard let ipv4Address = endpoints.first(where: \.isIPv4)?.host else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Self.hostnameLookupQueue.async {
|
||||||
|
let hostname = NearbyHostLookup.hostname(forIPv4: ipv4Address)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hostnameResolvers[resolverID] = resolver
|
||||||
|
resolver.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum NearbyHostLookup {
|
||||||
|
static func hostname(forIPv4 address: String) -> String? {
|
||||||
|
reverseDNSName(forIPv4: address).flatMap(prettifyDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func reverseDNSName(forIPv4 address: String) -> String? {
|
||||||
|
var socketAddress = sockaddr_in()
|
||||||
|
socketAddress.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
||||||
|
socketAddress.sin_family = sa_family_t(AF_INET)
|
||||||
|
|
||||||
|
guard inet_pton(AF_INET, address, &socketAddress.sin_addr) == 1 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
||||||
|
let result = withUnsafePointer(to: &socketAddress) { pointer in
|
||||||
|
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockaddrPointer in
|
||||||
|
getnameinfo(
|
||||||
|
sockaddrPointer,
|
||||||
|
socklen_t(MemoryLayout<sockaddr_in>.size),
|
||||||
|
&hostBuffer,
|
||||||
|
socklen_t(hostBuffer.count),
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
NI_NAMEREQD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard result == 0 else { return nil }
|
||||||
|
return String(cString: hostBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func prettifyDisplayName(_ value: String?) -> String? {
|
||||||
|
guard let value = value?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let withoutZone = value.split(separator: "%").first.map(String.init) ?? value
|
||||||
|
let withoutDomain = withoutZone.split(separator: ".").first.map(String.init) ?? withoutZone
|
||||||
|
let prettified = withoutDomain
|
||||||
|
.replacingOccurrences(of: "-", with: " ")
|
||||||
|
.replacingOccurrences(of: "_", with: " ")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
guard !prettified.isEmpty else { return nil }
|
||||||
|
return prettified.capitalized
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,11 +22,14 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
private var currentState: State = .initial
|
private var currentState: State = .initial
|
||||||
private let urlsToSend: [URL]
|
private let urlsToSend: [URL]
|
||||||
private let textToSend: String?
|
private let textToSend: String?
|
||||||
|
private let isClipboardSyncTransfer: Bool
|
||||||
|
private let receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
private var ukeyClientFinishMsgData: Data?
|
private var ukeyClientFinishMsgData: Data?
|
||||||
private var queue: [OutgoingFileTransfer] = []
|
private var queue: [OutgoingFileTransfer] = []
|
||||||
private var currentTransfer: OutgoingFileTransfer?
|
private var currentTransfer: OutgoingFileTransfer?
|
||||||
private var totalBytesToSend: Int64 = 0
|
private var totalBytesToSend: Int64 = 0
|
||||||
private var textPayloadID: Int64 = 0
|
private var textPayloadID: Int64 = 0
|
||||||
|
private var receiverAuthenticationVerified = false
|
||||||
|
|
||||||
public var qrCodePrivateKey: ECPrivateKey?
|
public var qrCodePrivateKey: ECPrivateKey?
|
||||||
public var delegate: OutboundNearbyConnectionDelegate?
|
public var delegate: OutboundNearbyConnectionDelegate?
|
||||||
@@ -35,10 +38,19 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
|
case initial, sentUkeyClientInit, sentUkeyClientFinish, sentPairedKeyEncryption, sentPairedKeyResult, sentIntroduction, sendingFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
init(connection: NWConnection, id: String, urlsToSend: [URL], textToSend: String?) {
|
init(
|
||||||
|
connection: NWConnection,
|
||||||
|
id: String,
|
||||||
|
urlsToSend: [URL],
|
||||||
|
textToSend: String?,
|
||||||
|
isClipboardSyncTransfer: Bool = false,
|
||||||
|
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
|
||||||
|
) {
|
||||||
|
|
||||||
self.urlsToSend = urlsToSend
|
self.urlsToSend = urlsToSend
|
||||||
self.textToSend = textToSend
|
self.textToSend = textToSend
|
||||||
|
self.isClipboardSyncTransfer = isClipboardSyncTransfer
|
||||||
|
self.receiverAuthenticationPolicy = receiverAuthenticationPolicy
|
||||||
|
|
||||||
super.init(connection: connection, id: id)
|
super.init(connection: connection, id: id)
|
||||||
|
|
||||||
@@ -368,7 +380,12 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
pairedResult.v1 = Sharing_Nearby_V1Frame()
|
pairedResult.v1 = Sharing_Nearby_V1Frame()
|
||||||
pairedResult.v1.type = .pairedKeyResult
|
pairedResult.v1.type = .pairedKeyResult
|
||||||
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
|
pairedResult.v1.pairedKeyResult = Sharing_Nearby_PairedKeyResultFrame()
|
||||||
|
if receiverAuthenticationPolicy.requiresReceiverAuthentication {
|
||||||
|
try verifyReceiverAuthentication(frame: frame)
|
||||||
|
pairedResult.v1.pairedKeyResult.status = .success
|
||||||
|
} else {
|
||||||
pairedResult.v1.pairedKeyResult.status = .unable
|
pairedResult.v1.pairedKeyResult.status = .unable
|
||||||
|
}
|
||||||
|
|
||||||
try sendTransferSetupFrame(pairedResult)
|
try sendTransferSetupFrame(pairedResult)
|
||||||
currentState = .sentPairedKeyResult
|
currentState = .sentPairedKeyResult
|
||||||
@@ -377,10 +394,16 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
|
private func processPairedKeyResult(frame: Sharing_Nearby_Frame) throws {
|
||||||
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
|
guard frame.hasV1, frame.v1.hasPairedKeyResult else { throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult") }
|
||||||
|
if receiverAuthenticationPolicy.requiresReceiverAuthentication, !receiverAuthenticationVerified {
|
||||||
|
throw NearbyError.protocolError("Receiver authentication incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
var introduction = Sharing_Nearby_Frame()
|
var introduction = Sharing_Nearby_Frame()
|
||||||
introduction.version = .v1
|
introduction.version = .v1
|
||||||
introduction.v1.type = .introduction
|
introduction.v1.type = .introduction
|
||||||
|
if isClipboardSyncTransfer {
|
||||||
|
introduction.v1.introduction.useCase = .remoteCopy
|
||||||
|
}
|
||||||
|
|
||||||
if let textToSend = textToSend {
|
if let textToSend = textToSend {
|
||||||
var meta = Sharing_Nearby_TextMetadata()
|
var meta = Sharing_Nearby_TextMetadata()
|
||||||
@@ -473,6 +496,84 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func verifyReceiverAuthentication(frame: Sharing_Nearby_Frame) throws {
|
||||||
|
guard let expectedFingerprint = receiverAuthenticationPolicy.expectedReceiverKeyFingerprint, !expectedFingerprint.isEmpty else {
|
||||||
|
throw NearbyError.protocolError("Missing expected receiver fingerprint")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let certificate = frame.v1.certificateInfo.publicCertificate.first else {
|
||||||
|
throw NearbyError.protocolError("Missing receiver certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
let certificateData = try certificate.serializedData()
|
||||||
|
let secretID = certificate.secretID
|
||||||
|
guard !secretID.isEmpty else {
|
||||||
|
throw NearbyError.protocolError("Missing receiver key ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
let peerGenericKey = try Securemessage_GenericPublicKey(serializedBytes: certificate.publicKey)
|
||||||
|
guard let derivedKeyID = peerGenericKey.id() else {
|
||||||
|
throw NearbyError.protocolError("Receiver key ID derivation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard secretID == derivedKeyID else {
|
||||||
|
throw NearbyError.protocolError("Receiver key ID mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
let actualFingerprint = derivedKeyID.hex.lowercased()
|
||||||
|
guard actualFingerprint == expectedFingerprint else {
|
||||||
|
throw NearbyError.protocolError("Receiver fingerprint mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
let pinnedCertificateData = TrustStore.shared.findTrustedKey(for: secretID)
|
||||||
|
if pinnedCertificateData == nil, !receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
|
||||||
|
throw NearbyError.protocolError("Receiver certificate is not trusted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pinnedCertificateData, pinnedCertificateData != certificateData {
|
||||||
|
throw NearbyError.protocolError("Receiver certificate changed")
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairedKeyEncryption = frame.v1.pairedKeyEncryption
|
||||||
|
guard pairedKeyEncryption.secretIDHash == secretID else {
|
||||||
|
throw NearbyError.protocolError("Receiver sent unexpected key ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let authKeyData = authKey?.data() else {
|
||||||
|
throw NearbyError.protocolError("Missing auth key for receiver verification")
|
||||||
|
}
|
||||||
|
|
||||||
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
|
let ecKey = peerGenericKey.ecP256PublicKey
|
||||||
|
let point = Point(
|
||||||
|
BInt(magnitude: [UInt8](ecKey.x.ensureLength(length: 32))),
|
||||||
|
BInt(magnitude: [UInt8](ecKey.y.ensureLength(length: 32)))
|
||||||
|
)
|
||||||
|
|
||||||
|
guard let peerPublicKey = try? ECPublicKey(domain: domain, w: point) else {
|
||||||
|
throw NearbyError.protocolError("Invalid receiver public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = try ECSignature(asn1: .build(pairedKeyEncryption.signedData), domain: domain)
|
||||||
|
guard peerPublicKey.verify(signature: signature, msg: authKeyData) else {
|
||||||
|
throw NearbyError.protocolError("Receiver signature verification failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pinnedCertificateData == nil, receiverAuthenticationPolicy.allowsReceiverTrustBootstrap {
|
||||||
|
guard let device = remoteDeviceInfo else {
|
||||||
|
throw NearbyError.protocolError("Missing remote device info for receiver trust")
|
||||||
|
}
|
||||||
|
delegate?.receiverAuthenticationPending(
|
||||||
|
connection: self,
|
||||||
|
certificateData: certificateData,
|
||||||
|
device: device,
|
||||||
|
fingerprint: actualFingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
receiverAuthenticationVerified = true
|
||||||
|
}
|
||||||
|
|
||||||
private func hasURL() -> Bool {
|
private func hasURL() -> Bool {
|
||||||
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
|
urlsToSend.count == 1 && !urlsToSend[0].isFileURL
|
||||||
}
|
}
|
||||||
@@ -593,3 +694,32 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
var currentOffset: Int64
|
var currentOffset: Int64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension ReceiverAuthenticationPolicy {
|
||||||
|
var expectedReceiverKeyFingerprint: String? {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
case .trustedReceiver(let fingerprint), .bootstrapReceiverTrust(let fingerprint):
|
||||||
|
return Self.normalizedFingerprint(fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowsReceiverTrustBootstrap: Bool {
|
||||||
|
if case .bootstrapReceiverTrust = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var requiresReceiverAuthentication: Bool {
|
||||||
|
if case .none = self {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static func normalizedFingerprint(_ fingerprint: String) -> String {
|
||||||
|
fingerprint.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ protocol InboundNearbyConnectionDelegate {
|
|||||||
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
|
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
|
||||||
func showPlusScreen()
|
func showPlusScreen()
|
||||||
func notificationSyncSetupConfirmed(connection: InboundNearbyConnection)
|
func pairingSetupConfirmed(connection: InboundNearbyConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -29,20 +29,33 @@ protocol OutboundNearbyConnectionDelegate {
|
|||||||
func transferAccepted(connection: OutboundNearbyConnection)
|
func transferAccepted(connection: OutboundNearbyConnection)
|
||||||
func failedWithError(connection: OutboundNearbyConnection, error: Error)
|
func failedWithError(connection: OutboundNearbyConnection, error: Error)
|
||||||
func transferFinished(connection: OutboundNearbyConnection)
|
func transferFinished(connection: OutboundNearbyConnection)
|
||||||
|
func receiverAuthenticationPending(
|
||||||
|
connection: OutboundNearbyConnection,
|
||||||
|
certificateData: Data,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
fingerprint: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum ReceiverAuthenticationPolicy: Equatable {
|
||||||
|
case none
|
||||||
|
case trustedReceiver(fingerprint: String)
|
||||||
|
case bootstrapReceiverTrust(fingerprint: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public protocol InboundAppDelegate: AnyObject {
|
public protocol InboundAppDelegate: AnyObject {
|
||||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: Error?)
|
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: Error?)
|
||||||
func transferProgress(connectionID: String, progress: Double)
|
func transferProgress(connectionID: String, progress: Double)
|
||||||
func showPlusScreen()
|
func showPlusScreen()
|
||||||
func notificationSyncSetupConfirmed()
|
func pairingSetupConfirmed()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension InboundAppDelegate {
|
extension InboundAppDelegate {
|
||||||
func notificationSyncSetupConfirmed() {}
|
func pairingSetupConfirmed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -62,20 +75,87 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
|
|||||||
public let name: String?
|
public let name: String?
|
||||||
public let type: DeviceType
|
public let type: DeviceType
|
||||||
public let qrCodeData: Data?
|
public let qrCodeData: Data?
|
||||||
|
public let isQuickDropPeer: Bool
|
||||||
public var id: String?
|
public var id: String?
|
||||||
|
public let keyFingerprint: String?
|
||||||
|
public let supportsNotificationSync: Bool
|
||||||
|
public let supportsClipboardReceive: Bool
|
||||||
|
|
||||||
init(name: String?, type: DeviceType?, id: String? = nil) {
|
init(
|
||||||
|
name: String?,
|
||||||
|
type: DeviceType?,
|
||||||
|
id: String? = nil,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type ?? .phone
|
self.type = type ?? .phone
|
||||||
self.id = id
|
self.id = id
|
||||||
self.qrCodeData = nil
|
self.qrCodeData = nil
|
||||||
|
self.isQuickDropPeer = isQuickDropPeer
|
||||||
|
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
|
||||||
|
self.supportsNotificationSync = supportsNotificationSync
|
||||||
|
self.supportsClipboardReceive = supportsClipboardReceive
|
||||||
}
|
}
|
||||||
|
|
||||||
init(info: EndpointInfo, id: String? = nil) {
|
init(
|
||||||
|
info: EndpointInfo,
|
||||||
|
id: String? = nil,
|
||||||
|
isQuickDropPeer: Bool = false,
|
||||||
|
keyFingerprint: String? = nil,
|
||||||
|
supportsNotificationSync: Bool = false,
|
||||||
|
supportsClipboardReceive: Bool = false
|
||||||
|
) {
|
||||||
self.name = info.name
|
self.name = info.name
|
||||||
self.type = info.deviceType
|
self.type = info.deviceType
|
||||||
self.qrCodeData = info.qrCodeData
|
self.qrCodeData = info.qrCodeData
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.isQuickDropPeer = isQuickDropPeer
|
||||||
|
self.keyFingerprint = Self.normalizedKeyFingerprint(keyFingerprint)
|
||||||
|
self.supportsNotificationSync = supportsNotificationSync
|
||||||
|
self.supportsClipboardReceive = supportsClipboardReceive
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case type
|
||||||
|
case qrCodeData
|
||||||
|
case isQuickDropPeer
|
||||||
|
case isVerifiedQuickDropPeer
|
||||||
|
case id
|
||||||
|
case keyFingerprint
|
||||||
|
case supportsNotificationSync
|
||||||
|
case supportsClipboardReceive
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
name = try container.decodeIfPresent(String.self, forKey: .name)
|
||||||
|
type = try container.decode(DeviceType.self, forKey: .type)
|
||||||
|
qrCodeData = try container.decodeIfPresent(Data.self, forKey: .qrCodeData)
|
||||||
|
isQuickDropPeer = try container.decodeIfPresent(Bool.self, forKey: .isQuickDropPeer)
|
||||||
|
?? container.decodeIfPresent(Bool.self, forKey: .isVerifiedQuickDropPeer)
|
||||||
|
?? false
|
||||||
|
id = try container.decodeIfPresent(String.self, forKey: .id)
|
||||||
|
keyFingerprint = Self.normalizedKeyFingerprint(
|
||||||
|
try container.decodeIfPresent(String.self, forKey: .keyFingerprint)
|
||||||
|
)
|
||||||
|
supportsNotificationSync = try container.decodeIfPresent(Bool.self, forKey: .supportsNotificationSync) ?? false
|
||||||
|
supportsClipboardReceive = try container.decodeIfPresent(Bool.self, forKey: .supportsClipboardReceive) ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encodeIfPresent(name, forKey: .name)
|
||||||
|
try container.encode(type, forKey: .type)
|
||||||
|
try container.encodeIfPresent(qrCodeData, forKey: .qrCodeData)
|
||||||
|
try container.encode(isQuickDropPeer, forKey: .isQuickDropPeer)
|
||||||
|
try container.encodeIfPresent(id, forKey: .id)
|
||||||
|
try container.encodeIfPresent(keyFingerprint, forKey: .keyFingerprint)
|
||||||
|
try container.encode(supportsNotificationSync, forKey: .supportsNotificationSync)
|
||||||
|
try container.encode(supportsClipboardReceive, forKey: .supportsClipboardReceive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -120,6 +200,13 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable {
|
|||||||
}
|
}
|
||||||
return "iphone"
|
return "iphone"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func normalizedKeyFingerprint(_ keyFingerprint: String?) -> String? {
|
||||||
|
let normalized = keyFingerprint?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
return normalized?.isEmpty == false ? normalized : nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -130,14 +217,16 @@ public struct TransferMetadata {
|
|||||||
public let textDescription: String?
|
public let textDescription: String?
|
||||||
public let type: TransferType
|
public let type: TransferType
|
||||||
public let allowsToBeAddedAsTrustedDevice: Bool
|
public let allowsToBeAddedAsTrustedDevice: Bool
|
||||||
|
public let isClipboardSync: Bool
|
||||||
|
|
||||||
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool) {
|
init(files: [FileMetadata], id: String, pinCode: String?, textDescription: String? = nil, transferType: TransferType = .file, allowsToBeAddedAsTrustedDevice: Bool, isClipboardSync: Bool = false) {
|
||||||
self.files = files
|
self.files = files
|
||||||
self.id = id
|
self.id = id
|
||||||
self.pinCode = pinCode
|
self.pinCode = pinCode
|
||||||
self.textDescription = textDescription
|
self.textDescription = textDescription
|
||||||
self.type = transferType
|
self.type = transferType
|
||||||
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
|
self.allowsToBeAddedAsTrustedDevice = allowsToBeAddedAsTrustedDevice
|
||||||
|
self.isClipboardSync = isClipboardSync
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TransferType {
|
public enum TransferType {
|
||||||
@@ -146,6 +235,7 @@ public struct TransferMetadata {
|
|||||||
case url
|
case url
|
||||||
case wifiPassword
|
case wifiPassword
|
||||||
case notificationSync
|
case notificationSync
|
||||||
|
case clipboardSync
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getSummary() -> String {
|
private func getSummary() -> String {
|
||||||
@@ -164,27 +254,27 @@ public struct TransferMetadata {
|
|||||||
let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext)
|
let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext)
|
||||||
|
|
||||||
if let type, type.conforms(to: .pdf) {
|
if let type, type.conforms(to: .pdf) {
|
||||||
return UTType.pdf.localizedDescription ?? "PDF"
|
return "PDF".localized()
|
||||||
}
|
}
|
||||||
if let type, type.conforms(to: .image) {
|
if let type, type.conforms(to: .image) {
|
||||||
return UTType.image.localizedDescription ?? "Image"
|
return "Image".localized()
|
||||||
}
|
}
|
||||||
if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
|
if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
|
||||||
return UTType.video.localizedDescription ?? "Video"
|
return "Video".localized()
|
||||||
}
|
}
|
||||||
if let type, type.conforms(to: .audio) {
|
if let type, type.conforms(to: .audio) {
|
||||||
return UTType.audio.localizedDescription ?? "Audio"
|
return "Audio".localized()
|
||||||
}
|
}
|
||||||
if let type, type.conforms(to: .text) {
|
if let type, type.conforms(to: .text) {
|
||||||
return UTType.text.localizedDescription ?? "Text"
|
return "Text".localized()
|
||||||
}
|
}
|
||||||
if let type, type.conforms(to: .archive) {
|
if let type, type.conforms(to: .archive) {
|
||||||
return UTType.archive.localizedDescription ?? "Archive"
|
return "Archive".localized()
|
||||||
}
|
}
|
||||||
if let type, let description = type.localizedDescription {
|
if let type, let description = type.localizedDescription {
|
||||||
return description
|
return description
|
||||||
}
|
}
|
||||||
return "File"
|
return "File".localized()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getDescription(deviceName: String) -> String {
|
public func getDescription(deviceName: String) -> String {
|
||||||
@@ -209,6 +299,12 @@ public struct TransferMetadata {
|
|||||||
return textDescription
|
return textDescription
|
||||||
}
|
}
|
||||||
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
|
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .clipboardSync:
|
||||||
|
if let textDescription, !textDescription.isEmpty {
|
||||||
|
return textDescription
|
||||||
|
}
|
||||||
|
return "ClipboardSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +313,7 @@ public struct TransferMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PairingUseCase: String {
|
enum PairingUseCase: String, Codable, Hashable {
|
||||||
case notificationSync = "notification_sync"
|
case notificationSync = "notification_sync"
|
||||||
case clipboardSync = "clipboard_sync"
|
case clipboardSync = "clipboard_sync"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//
|
//
|
||||||
// NotificationSyncPairingToken.swift
|
// PairingToken.swift
|
||||||
// QuickDrop
|
// QuickDrop
|
||||||
//
|
//
|
||||||
// Created by Codex on 2026-03-15.
|
// Created by Leon Böttger on 2026-03-15.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -10,7 +10,7 @@ import QRCode
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LUI
|
import LUI
|
||||||
|
|
||||||
enum NotificationSyncPairingToken {
|
enum PairingToken {
|
||||||
static func generate() -> String {
|
static func generate() -> String {
|
||||||
Data.randomData(length: 16).urlSafeBase64EncodedString()
|
Data.randomData(length: 16).urlSafeBase64EncodedString()
|
||||||
}
|
}
|
||||||
@@ -22,14 +22,14 @@ enum NotificationSyncPairingToken {
|
|||||||
return keyId.hex.lowercased()
|
return keyId.hex.lowercased()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase = .notificationSync) -> String {
|
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase) -> String {
|
||||||
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
|
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeQrImage(
|
static func makeQrImage(
|
||||||
token: String,
|
token: String,
|
||||||
receiverFingerprint: String,
|
receiverFingerprint: String,
|
||||||
useCase: PairingUseCase = .notificationSync,
|
useCase: PairingUseCase,
|
||||||
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
|
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
|
||||||
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
||||||
) -> Image? {
|
) -> Image? {
|
||||||
@@ -47,7 +47,7 @@ enum NotificationSyncPairingToken {
|
|||||||
.image(dimension: 1000)
|
.image(dimension: 1000)
|
||||||
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
|
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
|
||||||
} catch {
|
} catch {
|
||||||
log("[NotificationSyncPairingToken] QR code generation failed: \(error)")
|
log("[PairingToken] QR code generation failed: \(error)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import CryptoKit
|
|||||||
class TrustStore: ObservableObject {
|
class TrustStore: ObservableObject {
|
||||||
|
|
||||||
static let shared = TrustStore()
|
static let shared = TrustStore()
|
||||||
|
static let trustedCertificatesDidChange = Notification.Name("TrustStore.trustedCertificatesDidChange")
|
||||||
|
|
||||||
@Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:]
|
@Published private(set) var trustedCertificates: [String: TrustedCertificate] = [:]
|
||||||
|
|
||||||
@@ -58,12 +59,18 @@ class TrustStore: ObservableObject {
|
|||||||
do {
|
do {
|
||||||
try persistTrustedCertificates()
|
try persistTrustedCertificates()
|
||||||
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
|
notifyTrustedCertificatesDidChange()
|
||||||
} catch {
|
} catch {
|
||||||
print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
|
print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func notifyTrustedCertificatesDidChange() {
|
||||||
|
NotificationCenter.default.post(name: Self.trustedCertificatesDidChange, object: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Public API
|
// MARK: - Public API
|
||||||
|
|
||||||
func findTrustedKey(for secretIdHash: Data) -> Data? {
|
func findTrustedKey(for secretIdHash: Data) -> Data? {
|
||||||
@@ -71,19 +78,45 @@ class TrustStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func addTrusted(certificate: Sharing_Nearby_PublicCertificate, device: RemoteDeviceInfo) {
|
func addTrusted(
|
||||||
|
certificate: Sharing_Nearby_PublicCertificate,
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
grantedUseCases: Set<PairingUseCase> = []
|
||||||
|
) {
|
||||||
guard let publicKeyData = try? certificate.serializedData() else { return }
|
guard let publicKeyData = try? certificate.serializedData() else { return }
|
||||||
|
let fingerprint = certificate.secretID.hex
|
||||||
|
let existingGrants = trustedCertificates[fingerprint]?.grantedUseCases ?? []
|
||||||
|
|
||||||
trustedCertificates[certificate.secretID.hex] = TrustedCertificate(
|
trustedCertificates[fingerprint] = TrustedCertificate(
|
||||||
device: device,
|
device: device,
|
||||||
creationDate: Date(),
|
creationDate: Date(),
|
||||||
certificateData: publicKeyData
|
certificateData: publicKeyData,
|
||||||
|
grantedUseCases: existingGrants.union(grantedUseCases)
|
||||||
)
|
)
|
||||||
|
|
||||||
saveTrustedCertificates()
|
saveTrustedCertificates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func grantUseCase(_ useCase: PairingUseCase, for secretIdHex: String) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard let existing = trustedCertificates[normalized] else { return }
|
||||||
|
trustedCertificates[normalized] = existing.withGrantedUseCase(useCase)
|
||||||
|
saveTrustedCertificates()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func hasUseCaseGrant(_ useCase: PairingUseCase, for secretIdHex: String?) -> Bool {
|
||||||
|
guard let normalized = secretIdHex?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased(),
|
||||||
|
!normalized.isEmpty else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return trustedCertificates[normalized]?.grantedUseCases.contains(useCase) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func removeTrusted(secretIdHex: String) {
|
func removeTrusted(secretIdHex: String) {
|
||||||
trustedCertificates.removeValue(forKey: secretIdHex)
|
trustedCertificates.removeValue(forKey: secretIdHex)
|
||||||
saveTrustedCertificates()
|
saveTrustedCertificates()
|
||||||
@@ -142,6 +175,43 @@ class TrustStore: ObservableObject {
|
|||||||
let device: RemoteDeviceInfo
|
let device: RemoteDeviceInfo
|
||||||
let creationDate: Date
|
let creationDate: Date
|
||||||
let certificateData: Data
|
let certificateData: Data
|
||||||
|
let grantedUseCases: Set<PairingUseCase>
|
||||||
|
|
||||||
|
init(
|
||||||
|
device: RemoteDeviceInfo,
|
||||||
|
creationDate: Date,
|
||||||
|
certificateData: Data,
|
||||||
|
grantedUseCases: Set<PairingUseCase> = []
|
||||||
|
) {
|
||||||
|
self.device = device
|
||||||
|
self.creationDate = creationDate
|
||||||
|
self.certificateData = certificateData
|
||||||
|
self.grantedUseCases = grantedUseCases
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case device
|
||||||
|
case creationDate
|
||||||
|
case certificateData
|
||||||
|
case grantedUseCases
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
device = try container.decode(RemoteDeviceInfo.self, forKey: .device)
|
||||||
|
creationDate = try container.decode(Date.self, forKey: .creationDate)
|
||||||
|
certificateData = try container.decode(Data.self, forKey: .certificateData)
|
||||||
|
grantedUseCases = try container.decodeIfPresent(Set<PairingUseCase>.self, forKey: .grantedUseCases) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate {
|
||||||
|
TrustedCertificate(
|
||||||
|
device: device,
|
||||||
|
creationDate: creationDate,
|
||||||
|
certificateData: certificateData,
|
||||||
|
grantedUseCases: grantedUseCases.union([useCase])
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PendingPairingTrust: Equatable {
|
struct PendingPairingTrust: Equatable {
|
||||||
|
|||||||
@@ -20,13 +20,36 @@
|
|||||||
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
|
||||||
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; };
|
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||||
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; };
|
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */; };
|
||||||
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; };
|
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||||
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; };
|
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */; };
|
||||||
|
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
|
||||||
|
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
|
||||||
|
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
|
||||||
|
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
|
||||||
|
A3D000000000000000000202 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
|
||||||
|
A3D000000000000000000203 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
|
||||||
|
A6D000000000000000000101 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
|
||||||
|
A6D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
|
||||||
|
A6D000000000000000000201 /* AndroidAirDropModeDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */; };
|
||||||
|
A7D000000000000000000101 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
|
A7D000000000000000000102 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
|
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
|
A7D000000000000000000104 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
|
A8D000000000000000000101 /* BluetoothPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D000000000000000000002 /* BluetoothPermissionManager.swift */; };
|
||||||
|
A8D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */; };
|
||||||
|
A8D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */; };
|
||||||
|
A8D000000000000000000104 /* IssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBC05C7F2E33B58300EECC18 /* IssueView.swift */; };
|
||||||
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
|
||||||
|
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
|
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */; };
|
||||||
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D32E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
EB1325D52E701D8E00FD749C /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325D22E701D8900FD749C /* Settings.swift */; };
|
||||||
@@ -80,6 +103,7 @@
|
|||||||
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; };
|
||||||
|
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
|
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; };
|
||||||
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
EB3B9F1C2EF7150400D6704E /* SmallSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB96ED8A2D7F7CF4004B3FF1 /* SmallSheetView.swift */; };
|
||||||
@@ -148,6 +172,7 @@
|
|||||||
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; };
|
||||||
|
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; };
|
||||||
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; };
|
||||||
EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
EB3B9F722EF7151900D6704E /* ApIsolation6Tester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */; };
|
||||||
@@ -199,11 +224,13 @@
|
|||||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
||||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; };
|
||||||
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; };
|
||||||
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
|
EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; };
|
||||||
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
|
EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; };
|
||||||
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
|
EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; };
|
||||||
@@ -213,6 +240,7 @@
|
|||||||
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; };
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; };
|
||||||
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; };
|
||||||
@@ -231,6 +259,10 @@
|
|||||||
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C383F2E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C38402E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
EB5C38412E38C49D0052EFD7 /* LaunchAtLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */; };
|
||||||
|
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||||
|
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||||
|
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||||
|
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */; };
|
||||||
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
EB6E51A52F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||||
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
EB6E51A62F2F9370006D9812 /* IntroductionSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */; };
|
||||||
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
EB6E51AB2F311ED4006D9812 /* QuickDropIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = EB721F232E75AFA900F2099F /* QuickDropIcon.icon */; };
|
||||||
@@ -285,6 +317,7 @@
|
|||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; };
|
||||||
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; };
|
||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; };
|
||||||
|
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; };
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; };
|
||||||
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; };
|
||||||
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; };
|
||||||
@@ -366,6 +399,13 @@
|
|||||||
remoteGlobalIDString = 699DEBA02AB0573200115D22;
|
remoteGlobalIDString = 699DEBA02AB0573200115D22;
|
||||||
remoteInfo = ShareExtension;
|
remoteInfo = ShareExtension;
|
||||||
};
|
};
|
||||||
|
E14627D44A6E1CA296A9A53D /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 69DA9A0D29E0BF5100A442DA;
|
||||||
|
remoteInfo = QuickDrop;
|
||||||
|
};
|
||||||
EB3B9F8C2EF7153000D6704E /* PBXContainerItemProxy */ = {
|
EB3B9F8C2EF7153000D6704E /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */;
|
containerPortal = 69DA9A0629E0BF5100A442DA /* Project object */;
|
||||||
@@ -479,6 +519,7 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
156115A05E122702111639A0 /* QuickDropTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = QuickDropTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
691F53B92ABB70840089FD92 /* DeviceListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceListCell.swift; sourceTree = "<group>"; };
|
691F53B92ABB70840089FD92 /* DeviceListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceListCell.swift; sourceTree = "<group>"; };
|
||||||
691F53BA2ABB70840089FD92 /* DeviceListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeviceListCell.xib; sourceTree = "<group>"; };
|
691F53BA2ABB70840089FD92 /* DeviceListCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeviceListCell.xib; sourceTree = "<group>"; };
|
||||||
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundNearbyConnection.swift; sourceTree = "<group>"; };
|
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutboundNearbyConnection.swift; sourceTree = "<group>"; };
|
||||||
@@ -503,9 +544,16 @@
|
|||||||
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
69DA9A2C29E18CB500A442DA /* securegcm.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securegcm.pb.swift; sourceTree = "<group>"; };
|
||||||
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = securemessage.pb.swift; sourceTree = "<group>"; };
|
||||||
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
|
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
|
||||||
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncPairingToken.swift; sourceTree = "<group>"; };
|
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncCoordinator.swift; sourceTree = "<group>"; };
|
||||||
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncQrSheetManager.swift; sourceTree = "<group>"; };
|
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; };
|
||||||
|
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; };
|
||||||
|
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; };
|
||||||
|
A3D000000000000000000002 /* BluetoothPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothPermissionManager.swift; sourceTree = "<group>"; };
|
||||||
|
A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeDetector.swift; sourceTree = "<group>"; };
|
||||||
|
A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeDetectorTests.swift; sourceTree = "<group>"; };
|
||||||
|
A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidAirDropModeIssueView.swift; sourceTree = "<group>"; };
|
||||||
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
|
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
|
||||||
|
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyBonjourServiceResolver.swift; sourceTree = "<group>"; };
|
||||||
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||||
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
|
||||||
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
EB1325E82E7025D900FD749C /* ApIsolation6Tester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApIsolation6Tester.swift; sourceTree = "<group>"; };
|
||||||
@@ -518,6 +566,7 @@
|
|||||||
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
EB2B41E32DC63AC80070FC39 /* SleepManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepManager.swift; sourceTree = "<group>"; };
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = "<group>"; };
|
||||||
|
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceNameResolver.swift; sourceTree = "<group>"; };
|
||||||
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropGitHub.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
|
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "QuickDrop copy-Info.plist"; path = "/Users/leonboettger/QuickDrop Backup with git/QuickDrop copy-Info.plist"; sourceTree = "<absolute>"; };
|
||||||
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QuickDropShareExtensionGitHub.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -530,6 +579,7 @@
|
|||||||
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
EB5C37E22E377C2E0052EFD7 /* QuickDropiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickDropiOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
EB5C38382E38C1F60052EFD7 /* justMaybe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = justMaybe.mp3; sourceTree = "<group>"; };
|
||||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLogin.swift; sourceTree = "<group>"; };
|
||||||
|
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelessAlertPresenter.swift; sourceTree = "<group>"; };
|
||||||
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; };
|
EB6E51A42F2F9370006D9812 /* IntroductionSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionSheetManager.swift; sourceTree = "<group>"; };
|
||||||
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
EB6E6B202ECBA95000E71D57 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||||
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
@@ -653,6 +703,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
F5F5F63DAC90375051BA9F4E /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -693,6 +750,7 @@
|
|||||||
EBC05C8B2E33B59E00EECC18 /* Resources */,
|
EBC05C8B2E33B59E00EECC18 /* Resources */,
|
||||||
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */,
|
EB3B9F492EF7150400D6704E /* QuickDrop copy-Info.plist */,
|
||||||
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */,
|
EB3B9F8A2EF7151900D6704E /* QuickDropShareExtension copy-Info.plist */,
|
||||||
|
9E6A957675D279417AA88926 /* QuickDropTests */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -705,6 +763,7 @@
|
|||||||
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */,
|
EB950B2D2E52027F002D68FA /* iOSShareExtension.appex */,
|
||||||
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */,
|
EB3B9F482EF7150400D6704E /* QuickDropGitHub.app */,
|
||||||
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */,
|
EB3B9F892EF7151900D6704E /* QuickDropShareExtensionGitHub.appex */,
|
||||||
|
156115A05E122702111639A0 /* QuickDropTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -750,12 +809,20 @@
|
|||||||
path = NearbyShare;
|
path = NearbyShare;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */ = {
|
9E6A957675D279417AA88926 /* QuickDropTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */,
|
A6D000000000000000000002 /* AndroidAirDropModeDetectorTests.swift */,
|
||||||
);
|
);
|
||||||
path = NotificationSync;
|
path = QuickDropTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */,
|
||||||
|
);
|
||||||
|
path = Pairing;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
EB1325EA2E7025D900FD749C /* SelfTest */ = {
|
EB1325EA2E7025D900FD749C /* SelfTest */ = {
|
||||||
@@ -779,7 +846,7 @@
|
|||||||
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
EB886E512E4E19390052B7D9 /* HKDF.swift */,
|
||||||
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
|
||||||
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
|
||||||
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */,
|
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -788,6 +855,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
|
69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */,
|
||||||
|
EB0A22E22FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift */,
|
||||||
|
EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */,
|
||||||
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
|
69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */,
|
||||||
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
|
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */,
|
||||||
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
|
691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */,
|
||||||
@@ -911,7 +980,7 @@
|
|||||||
EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
|
EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
|
||||||
EB6E51A92F2F95EE006D9812 /* UserManual */,
|
EB6E51A92F2F95EE006D9812 /* UserManual */,
|
||||||
EB6E51AA2F2F9606006D9812 /* Toast */,
|
EB6E51AA2F2F9606006D9812 /* Toast */,
|
||||||
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */,
|
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */,
|
||||||
EB6E51A82F2F95E0006D9812 /* Settings */,
|
EB6E51A82F2F95E0006D9812 /* Settings */,
|
||||||
EB6E51A72F2F95D0006D9812 /* Introduction */,
|
EB6E51A72F2F95D0006D9812 /* Introduction */,
|
||||||
EBC05C812E33B58300EECC18 /* IssueViews */,
|
EBC05C812E33B58300EECC18 /* IssueViews */,
|
||||||
@@ -922,12 +991,17 @@
|
|||||||
EBC05C6B2E33B57100EECC18 /* Util */ = {
|
EBC05C6B2E33B57100EECC18 /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A3D000000000000000000002 /* BluetoothPermissionManager.swift */,
|
||||||
|
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */,
|
||||||
|
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */,
|
||||||
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
EB1325F72E70287C00FD749C /* SendToExtension.swift */,
|
||||||
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
EB5C383E2E38C49D0052EFD7 /* LaunchAtLogin.swift */,
|
||||||
|
EB63EDB62FF42390000CC61F /* ModelessAlertPresenter.swift */,
|
||||||
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
EB886E662E4F2F7C0052B7D9 /* License.swift */,
|
||||||
EB6FE7D12EFEFB2700FF2BCF /* AllowedWorkMonitor.swift */,
|
EB6FE7D12EFEFB2700FF2BCF /* AllowedWorkMonitor.swift */,
|
||||||
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
EB36435F2D73A29F009CDFF8 /* AudioManager.swift */,
|
||||||
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
EBC05C5A2E33936C00EECC18 /* ErrorAlertHandler.swift */,
|
||||||
|
A6D000000000000000000001 /* AndroidAirDropModeDetector.swift */,
|
||||||
EB1325EA2E7025D900FD749C /* SelfTest */,
|
EB1325EA2E7025D900FD749C /* SelfTest */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
@@ -940,6 +1014,7 @@
|
|||||||
EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */,
|
EBC05C7E2E33B58300EECC18 /* FirewallIssueView.swift */,
|
||||||
EBC05C7F2E33B58300EECC18 /* IssueView.swift */,
|
EBC05C7F2E33B58300EECC18 /* IssueView.swift */,
|
||||||
EBC05C802E33B58300EECC18 /* NetworkFilterIssueView.swift */,
|
EBC05C802E33B58300EECC18 /* NetworkFilterIssueView.swift */,
|
||||||
|
A7D000000000000000000001 /* AndroidAirDropModeIssueView.swift */,
|
||||||
);
|
);
|
||||||
path = IssueViews;
|
path = IssueViews;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -957,6 +1032,24 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
32A98F34E7D10C21DC2EEA19 /* QuickDropTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = B473F6C65C8455203EA5FD18 /* Build configuration list for PBXNativeTarget "QuickDropTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
BECDC1522AF83179791FA522 /* Sources */,
|
||||||
|
F5F5F63DAC90375051BA9F4E /* Frameworks */,
|
||||||
|
36CE9ED35EE7975FAB802535 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
C4F698F843B0C498BFC216AB /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = QuickDropTests;
|
||||||
|
productName = QuickDropTests;
|
||||||
|
productReference = 156115A05E122702111639A0 /* QuickDropTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
699DEBA02AB0573200115D22 /* QuickDropShareExtension */ = {
|
699DEBA02AB0573200115D22 /* QuickDropShareExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 699DEBB12AB0573200115D22 /* Build configuration list for PBXNativeTarget "QuickDropShareExtension" */;
|
buildConfigurationList = 699DEBB12AB0573200115D22 /* Build configuration list for PBXNativeTarget "QuickDropShareExtension" */;
|
||||||
@@ -1113,6 +1206,9 @@
|
|||||||
LastSwiftUpdateCheck = 1640;
|
LastSwiftUpdateCheck = 1640;
|
||||||
LastUpgradeCheck = 2600;
|
LastUpgradeCheck = 2600;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
32A98F34E7D10C21DC2EEA19 = {
|
||||||
|
CreatedOnToolsVersion = 26.0;
|
||||||
|
};
|
||||||
699DEBA02AB0573200115D22 = {
|
699DEBA02AB0573200115D22 = {
|
||||||
CreatedOnToolsVersion = 14.3.1;
|
CreatedOnToolsVersion = 14.3.1;
|
||||||
};
|
};
|
||||||
@@ -1172,11 +1268,19 @@
|
|||||||
EB950B2C2E52027F002D68FA /* iOSShareExtension */,
|
EB950B2C2E52027F002D68FA /* iOSShareExtension */,
|
||||||
EB3B9EF02EF7150400D6704E /* QuickDropGitHub */,
|
EB3B9EF02EF7150400D6704E /* QuickDropGitHub */,
|
||||||
EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */,
|
EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */,
|
||||||
|
32A98F34E7D10C21DC2EEA19 /* QuickDropTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
36CE9ED35EE7975FAB802535 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
699DEB9F2AB0573200115D22 /* Resources */ = {
|
699DEB9F2AB0573200115D22 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -1288,14 +1392,17 @@
|
|||||||
EBE5FF432EDB71810019B380 /* Configuration.swift in Sources */,
|
EBE5FF432EDB71810019B380 /* Configuration.swift in Sources */,
|
||||||
EB6FE7D42EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
EB6FE7D42EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
||||||
EBC05C872E33B58300EECC18 /* IssueView.swift in Sources */,
|
EBC05C872E33B58300EECC18 /* IssueView.swift in Sources */,
|
||||||
|
A7D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||||
EBC05C882E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
EBC05C882E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
||||||
EBC05C892E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
EBC05C892E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
||||||
|
EB63EDBB2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||||
699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */,
|
699DEBA62AB0573200115D22 /* ShareViewController.swift in Sources */,
|
||||||
EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */,
|
EB96ED8B2D7F7CF7004B3FF1 /* SmallSheetView.swift in Sources */,
|
||||||
EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */,
|
EBCC27F92D80C9F90079C478 /* Zip.swift in Sources */,
|
||||||
EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
EB5C37C52E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
||||||
EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37C62E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
EB5C37C72E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E42FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37C82E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37C92E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
EB950B742E520957002D68FA /* Attachments.swift in Sources */,
|
||||||
@@ -1315,6 +1422,7 @@
|
|||||||
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
|
EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
@@ -1337,6 +1445,8 @@
|
|||||||
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
EB16B6182E58B4C8007D9792 /* TrustStore.swift in Sources */,
|
||||||
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */,
|
EBC05C5B2E33937200EECC18 /* ErrorAlertHandler.swift in Sources */,
|
||||||
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */,
|
EB1325D42E701D8E00FD749C /* Settings.swift in Sources */,
|
||||||
|
A3D000000000000000000202 /* BluetoothPermissionManager.swift in Sources */,
|
||||||
|
A6D000000000000000000101 /* AndroidAirDropModeDetector.swift in Sources */,
|
||||||
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
EB886E5D2E4F13EB0052B7D9 /* Zip.swift in Sources */,
|
||||||
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
EB5C37BF2E377A780052EFD7 /* ukey.pb.swift in Sources */,
|
||||||
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
EB5C37C02E377A780052EFD7 /* securemessage.pb.swift in Sources */,
|
||||||
@@ -1344,16 +1454,19 @@
|
|||||||
EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37C22E377A780052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
EB5C37C32E377A780052EFD7 /* wire_format.pb.swift in Sources */,
|
||||||
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
|
EB63EDB92FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||||
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
|
||||||
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */,
|
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
|
||||||
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */,
|
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */,
|
||||||
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
|
||||||
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
|
||||||
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||||
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
|
A1C0DE132F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||||
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
|
EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */,
|
||||||
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
|
EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */,
|
||||||
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
|
EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||||
@@ -1370,13 +1483,24 @@
|
|||||||
EB6FE7D62EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
EB6FE7D62EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
||||||
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
EBC05C822E33B58300EECC18 /* ApIsolationIssueView.swift in Sources */,
|
||||||
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
EBC05C832E33B58300EECC18 /* FirewallIssueView.swift in Sources */,
|
||||||
|
A7D000000000000000000101 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||||
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
|
EB1325FA2E70288300FD749C /* SendToExtension.swift in Sources */,
|
||||||
|
EB0A22E72FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
EB1325F42E7025D900FD749C /* ApIsolationTester.swift in Sources */,
|
||||||
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
EB1325F52E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
EB1325F62E7025D900FD749C /* ApIsolation6Tester.swift in Sources */,
|
||||||
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
|
EBC05C842E33B58300EECC18 /* IssueView.swift in Sources */,
|
||||||
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
EBC05C852E33B58300EECC18 /* NetworkFilterIssueView.swift in Sources */,
|
||||||
EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */,
|
EB7A95F42EF6E1BE0021111B /* IntroductionView.swift in Sources */,
|
||||||
|
A1C0DE112F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
BECDC1522AF83179791FA522 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
A6D000000000000000000201 /* AndroidAirDropModeDetectorTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1394,6 +1518,8 @@
|
|||||||
EB3B9F082EF7150400D6704E /* TrustStore.swift in Sources */,
|
EB3B9F082EF7150400D6704E /* TrustStore.swift in Sources */,
|
||||||
EB3B9F092EF7150400D6704E /* ErrorAlertHandler.swift in Sources */,
|
EB3B9F092EF7150400D6704E /* ErrorAlertHandler.swift in Sources */,
|
||||||
EB3B9F0A2EF7150400D6704E /* Settings.swift in Sources */,
|
EB3B9F0A2EF7150400D6704E /* Settings.swift in Sources */,
|
||||||
|
A3D000000000000000000203 /* BluetoothPermissionManager.swift in Sources */,
|
||||||
|
A6D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */,
|
||||||
EB3B9F0B2EF7150400D6704E /* Zip.swift in Sources */,
|
EB3B9F0B2EF7150400D6704E /* Zip.swift in Sources */,
|
||||||
EB3B9F0C2EF7150400D6704E /* ukey.pb.swift in Sources */,
|
EB3B9F0C2EF7150400D6704E /* ukey.pb.swift in Sources */,
|
||||||
EB3B9F0D2EF7150400D6704E /* securemessage.pb.swift in Sources */,
|
EB3B9F0D2EF7150400D6704E /* securemessage.pb.swift in Sources */,
|
||||||
@@ -1401,16 +1527,19 @@
|
|||||||
EB3B9F0F2EF7150400D6704E /* device_to_device_messages.pb.swift in Sources */,
|
EB3B9F0F2EF7150400D6704E /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB3B9F102EF7150400D6704E /* wire_format.pb.swift in Sources */,
|
EB3B9F102EF7150400D6704E /* wire_format.pb.swift in Sources */,
|
||||||
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
|
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
|
||||||
|
EB63EDB72FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||||
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
|
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
|
||||||
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
|
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
|
||||||
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */,
|
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
|
||||||
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */,
|
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */,
|
||||||
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
|
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
|
||||||
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
|
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
|
||||||
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
|
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
|
||||||
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
|
EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */,
|
||||||
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
|
A1C0DE142F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift in Sources */,
|
||||||
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
|
EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
|
EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */,
|
||||||
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
|
EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */,
|
||||||
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
|
EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */,
|
||||||
@@ -1427,13 +1556,16 @@
|
|||||||
EB6FE7D52EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
EB6FE7D52EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
||||||
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
|
EB3B9F272EF7150400D6704E /* ApIsolationIssueView.swift in Sources */,
|
||||||
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
|
EB3B9F282EF7150400D6704E /* FirewallIssueView.swift in Sources */,
|
||||||
|
A7D000000000000000000102 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||||
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
|
EB3B9F292EF7150400D6704E /* SendToExtension.swift in Sources */,
|
||||||
|
EB0A22E82FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
|
EB3B9F2A2EF7150400D6704E /* ApIsolationTester.swift in Sources */,
|
||||||
EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */,
|
EB3B9F2B2EF7150400D6704E /* ActiveNetworkInterface.swift in Sources */,
|
||||||
EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */,
|
EB3B9F2C2EF7150400D6704E /* ApIsolation6Tester.swift in Sources */,
|
||||||
EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */,
|
EB3B9F2D2EF7150400D6704E /* IssueView.swift in Sources */,
|
||||||
EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */,
|
EB3B9F2E2EF7150400D6704E /* NetworkFilterIssueView.swift in Sources */,
|
||||||
EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */,
|
EB3B9F2F2EF7150400D6704E /* IntroductionView.swift in Sources */,
|
||||||
|
A1C0DE122F4C000100ABC001 /* ClipboardSyncCoordinator.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1449,14 +1581,17 @@
|
|||||||
EB3B9F522EF7151900D6704E /* Configuration.swift in Sources */,
|
EB3B9F522EF7151900D6704E /* Configuration.swift in Sources */,
|
||||||
EB6FE7D22EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
EB6FE7D22EFEFB2700FF2BCF /* AllowedWorkMonitor.swift in Sources */,
|
||||||
EB3B9F532EF7151900D6704E /* IssueView.swift in Sources */,
|
EB3B9F532EF7151900D6704E /* IssueView.swift in Sources */,
|
||||||
|
A7D000000000000000000104 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||||
EB3B9F542EF7151900D6704E /* FirewallIssueView.swift in Sources */,
|
EB3B9F542EF7151900D6704E /* FirewallIssueView.swift in Sources */,
|
||||||
EB3B9F552EF7151900D6704E /* ApIsolationIssueView.swift in Sources */,
|
EB3B9F552EF7151900D6704E /* ApIsolationIssueView.swift in Sources */,
|
||||||
|
EB63EDBA2FF42390000CC61F /* ModelessAlertPresenter.swift in Sources */,
|
||||||
EB3B9F562EF7151900D6704E /* ShareViewController.swift in Sources */,
|
EB3B9F562EF7151900D6704E /* ShareViewController.swift in Sources */,
|
||||||
EB3B9F572EF7151900D6704E /* SmallSheetView.swift in Sources */,
|
EB3B9F572EF7151900D6704E /* SmallSheetView.swift in Sources */,
|
||||||
EB3B9F582EF7151900D6704E /* Zip.swift in Sources */,
|
EB3B9F582EF7151900D6704E /* Zip.swift in Sources */,
|
||||||
EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */,
|
EB3B9F592EF7151900D6704E /* securemessage.pb.swift in Sources */,
|
||||||
EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */,
|
EB3B9F5A2EF7151900D6704E /* securegcm.pb.swift in Sources */,
|
||||||
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
|
EB3B9F5B2EF7151900D6704E /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E52FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
|
EB3B9F5C2EF7151900D6704E /* ukey.pb.swift in Sources */,
|
||||||
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
|
EB3B9F5D2EF7151900D6704E /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
|
EB3B9F5F2EF7151900D6704E /* Attachments.swift in Sources */,
|
||||||
@@ -1476,6 +1611,7 @@
|
|||||||
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
|
EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */,
|
||||||
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
|
EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
|
EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */,
|
||||||
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
|
EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */,
|
||||||
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
|
EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */,
|
||||||
@@ -1496,13 +1632,17 @@
|
|||||||
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
EB886E672E4F2F800052B7D9 /* License.swift in Sources */,
|
||||||
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
|
EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */,
|
||||||
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */,
|
||||||
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
|
EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */,
|
||||||
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */,
|
EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */,
|
||||||
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
|
EB5C37F92E377CE70052EFD7 /* securegcm.pb.swift in Sources */,
|
||||||
|
A8D000000000000000000101 /* BluetoothPermissionManager.swift in Sources */,
|
||||||
|
A8D000000000000000000102 /* AndroidAirDropModeDetector.swift in Sources */,
|
||||||
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
|
EB5C37FA2E377CE70052EFD7 /* Helpers.swift in Sources */,
|
||||||
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
EB5C37FB2E377CE70052EFD7 /* InboundNearbyConnection.swift in Sources */,
|
||||||
|
EB0A22E62FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */,
|
||||||
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
EB9925802E45D53E00357055 /* sharing_enums.pb.swift in Sources */,
|
||||||
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
EB5C37FC2E377CE70052EFD7 /* OutboundNearbyConnection.swift in Sources */,
|
||||||
@@ -1516,6 +1656,8 @@
|
|||||||
EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */,
|
EB979F942E50CA23008B8C5C /* Configuration.swift in Sources */,
|
||||||
EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */,
|
EB979F952E50CA23008B8C5C /* ContentView.swift in Sources */,
|
||||||
EB6FE7CF2EFEB42800FF2BCF /* NoLocalNetworkAccessAlertView.swift in Sources */,
|
EB6FE7CF2EFEB42800FF2BCF /* NoLocalNetworkAccessAlertView.swift in Sources */,
|
||||||
|
A8D000000000000000000103 /* AndroidAirDropModeIssueView.swift in Sources */,
|
||||||
|
A8D000000000000000000104 /* IssueView.swift in Sources */,
|
||||||
EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */,
|
EBBEE1462EC9D93400AA7831 /* PhotoManager.swift in Sources */,
|
||||||
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
|
EB97A01A2E51EAA2008B8C5C /* UIAlert.swift in Sources */,
|
||||||
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
|
EB979F962E50CA23008B8C5C /* FilePicker.swift in Sources */,
|
||||||
@@ -1547,10 +1689,12 @@
|
|||||||
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
EB16B6132E58B4B9007D9792 /* IdentityManager.swift in Sources */,
|
||||||
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
EB950B4D2E520436002D68FA /* NearbyConnection.swift in Sources */,
|
||||||
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */,
|
||||||
|
EB0A22E32FBCF59300B78FF5 /* NearbyBonjourServiceResolver.swift in Sources */,
|
||||||
EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
|
EB950B582E5205B4002D68FA /* Zip.swift in Sources */,
|
||||||
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
|
EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */,
|
||||||
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
|
EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */,
|
||||||
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
|
EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */,
|
||||||
|
EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */,
|
||||||
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
EB950B532E520436002D68FA /* ukey.pb.swift in Sources */,
|
||||||
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */,
|
||||||
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */,
|
||||||
@@ -1582,6 +1726,12 @@
|
|||||||
target = 699DEBA02AB0573200115D22 /* QuickDropShareExtension */;
|
target = 699DEBA02AB0573200115D22 /* QuickDropShareExtension */;
|
||||||
targetProxy = 699DEBAC2AB0573200115D22 /* PBXContainerItemProxy */;
|
targetProxy = 699DEBAC2AB0573200115D22 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
C4F698F843B0C498BFC216AB /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
name = QuickDrop;
|
||||||
|
target = 69DA9A0D29E0BF5100A442DA /* QuickDrop */;
|
||||||
|
targetProxy = E14627D44A6E1CA296A9A53D /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
EB3B9F8D2EF7153000D6704E /* PBXTargetDependency */ = {
|
EB3B9F8D2EF7153000D6704E /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */;
|
target = EB3B9F4A2EF7151900D6704E /* QuickDropShareExtensionGitHub */;
|
||||||
@@ -1706,7 +1856,6 @@
|
|||||||
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = EXTENSION;
|
||||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = "";
|
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
@@ -1773,8 +1922,8 @@
|
|||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 138;
|
SHARED_BUILD = 154;
|
||||||
SHARED_VERSION = 3.0;
|
SHARED_VERSION = 4.2.1;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1836,8 +1985,8 @@
|
|||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SHARED_BUILD = 138;
|
SHARED_BUILD = 154;
|
||||||
SHARED_VERSION = 3.0;
|
SHARED_VERSION = 4.2.1;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
@@ -1865,7 +2014,7 @@
|
|||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
@@ -1880,6 +2029,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
@@ -1930,7 +2081,7 @@
|
|||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
@@ -1945,6 +2096,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
@@ -1974,6 +2127,49 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
9A4E1D6A03B4FDA5B2074DF0 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = 92SDAWLN76;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.tests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/QuickDrop.app/Contents/MacOS/QuickDrop";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
A8C87F63E411A0C639408AC5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = 92SDAWLN76;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
"@loader_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 12.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.leonboettger.neardrop.tests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/QuickDrop.app/Contents/MacOS/QuickDrop";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
EB3B9F462EF7150400D6704E /* Debug */ = {
|
EB3B9F462EF7150400D6704E /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -1994,7 +2190,7 @@
|
|||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
@@ -2009,6 +2205,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
@@ -2060,7 +2258,7 @@
|
|||||||
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
ENABLE_INCOMING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
|
ENABLE_RESOURCE_ACCESS_BLUETOOTH = YES;
|
||||||
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
ENABLE_RESOURCE_ACCESS_CAMERA = NO;
|
||||||
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
|
||||||
@@ -2075,6 +2273,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSUIElement = YES;
|
INFOPLIST_KEY_LSUIElement = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
@@ -2225,6 +2425,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -2270,6 +2472,8 @@
|
|||||||
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||||
|
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
|
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "QuickDrop uses Bluetooth for enhanced nearby device detection.";
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2025-2026 Leon Böttger";
|
||||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Local network access is needed to detect devices and transfer files over your Wi-Fi network.";
|
||||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "The add photos permission is required to save received photos and videos to your photo library.";
|
||||||
@@ -2392,6 +2596,15 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
B473F6C65C8455203EA5FD18 /* Build configuration list for PBXNativeTarget "QuickDropTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
9A4E1D6A03B4FDA5B2074DF0 /* Release */,
|
||||||
|
A8C87F63E411A0C639408AC5 /* Debug */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
EB3B9F452EF7150400D6704E /* Build configuration list for PBXNativeTarget "QuickDropGitHub" */ = {
|
EB3B9F452EF7150400D6704E /* Build configuration list for PBXNativeTarget "QuickDropGitHub" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/leif-ibsen/BigInt",
|
"location" : "https://github.com/leif-ibsen/BigInt",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "8c6f93aa37504b7b1ba3954335b5548a19fbbd82",
|
"revision" : "7c7b1be2f6714ce03833879b0aab4b20293c0207",
|
||||||
"version" : "1.22.0"
|
"version" : "1.24.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"location" : "https://github.com/leonboe1/LUI.git",
|
"location" : "https://github.com/leonboe1/LUI.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "3582f59e758c39a849ab691f3fe26f7d0f618a94"
|
"revision" : "44013e8ef77d8e0d18e13f449cfe3775f1abb31a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,8 +87,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6",
|
"revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8",
|
||||||
"version" : "1.36.1"
|
"version" : "1.38.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "26986a57e31c813f98262ddb62d4b07d40008535"
|
"revision" : "aee624c87acee41e1a421f7825b468d38bbfbd6c"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,24 @@
|
|||||||
debugDocumentVersioning = "YES"
|
debugDocumentVersioning = "YES"
|
||||||
debugServiceExtension = "internal"
|
debugServiceExtension = "internal"
|
||||||
allowLocationSimulation = "YES">
|
allowLocationSimulation = "YES">
|
||||||
|
<PostActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Register dev QuickDrop for Finder Share"
|
||||||
|
scriptText = ""${SRCROOT}/scripts/use-dev-share-extension.sh" ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "69DA9A0D29E0BF5100A442DA"
|
||||||
|
BuildableName = "QuickDrop.app"
|
||||||
|
BlueprintName = "QuickDrop"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PostActions>
|
||||||
<BuildableProductRunnable
|
<BuildableProductRunnable
|
||||||
runnableDebuggingMode = "0">
|
runnableDebuggingMode = "0">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2650"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "32A98F34E7D10C21DC2EEA19"
|
||||||
|
BuildableName = "QuickDropTests.xctest"
|
||||||
|
BlueprintName = "QuickDropTests"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnableBacktraceRecording = "Yes">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = ""
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import BezelNotification
|
import BezelNotification
|
||||||
|
import Combine
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Network
|
import Network
|
||||||
import StoreKit
|
import StoreKit
|
||||||
@@ -27,14 +28,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
private var errorAlertHandler = ErrorAlertHandler.shared
|
private var errorAlertHandler = ErrorAlertHandler.shared
|
||||||
|
|
||||||
private var receiveModel: ReceiveModel?
|
private var receiveModel: ReceiveModel?
|
||||||
|
private var bluetoothAuthorizationObserver: AnyCancellable?
|
||||||
|
private let welcomeNavigationState = WelcomeScreenNavigationState()
|
||||||
|
|
||||||
var showsFirewallAlert = false
|
var showsFirewallAlert = false
|
||||||
var visibleItem: NSMenuItem? = nil
|
var visibleItem: NSMenuItem? = nil
|
||||||
|
private var didShowAndroidAirDropModeAlert = false
|
||||||
|
|
||||||
|
|
||||||
// MARK: NSApplicationDelegate functions
|
// MARK: NSApplicationDelegate functions
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_: Notification) {
|
func applicationDidFinishLaunching(_: Notification) {
|
||||||
|
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
|
||||||
|
LUIInit(configuration: configuration)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UNUserNotificationCenter.current().delegate = self
|
||||||
|
|
||||||
// app did not lauch before
|
// app did not lauch before
|
||||||
if !Settings.sharedInstance.appLaunchedBefore {
|
if !Settings.sharedInstance.appLaunchedBefore {
|
||||||
runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") }
|
runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") }
|
||||||
@@ -66,18 +77,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
startReceiving()
|
startReceiving()
|
||||||
}
|
}
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: Menu Bar
|
// MARK: Menu Bar
|
||||||
|
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
|
|
||||||
let visibleItem = NSMenuItem(
|
let visibleItem = NSMenuItem(
|
||||||
title: getDefaultVisibleLabel(),
|
title: getDefaultVisibleLabel(),
|
||||||
action: nil,
|
action: #selector(visibleMenuItemSelected),
|
||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
|
visibleItem.target = self
|
||||||
self.visibleItem = visibleItem
|
self.visibleItem = visibleItem
|
||||||
menu.addItem(visibleItem)
|
menu.addItem(visibleItem)
|
||||||
|
|
||||||
@@ -120,6 +129,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupAndroidAirDropModeDetection()
|
||||||
|
requestBluetoothPermissionForExistingUserIfNeeded()
|
||||||
|
|
||||||
|
|
||||||
// MARK: Logging
|
// MARK: Logging
|
||||||
|
|
||||||
@@ -131,6 +143,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
func startReceiving() {
|
func startReceiving() {
|
||||||
guard receiveModel == nil else {
|
guard receiveModel == nil else {
|
||||||
log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.")
|
log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.")
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +159,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -166,14 +181,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
|
|
||||||
public func userNotificationCenter(_: UNUserNotificationCenter,
|
public func userNotificationCenter(_: UNUserNotificationCenter,
|
||||||
willPresent _: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
|
if ClipboardSyncNotificationPresenter.isClipboardSyncNotification(notification) {
|
||||||
|
completionHandler([.banner])
|
||||||
|
return
|
||||||
|
}
|
||||||
completionHandler([.sound, .banner])
|
completionHandler([.sound, .banner])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func applicationWillTerminate(_: Notification) {
|
func applicationWillTerminate(_: Notification) {
|
||||||
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||||
|
ClipboardSyncCoordinator.shared.stop()
|
||||||
|
AndroidAirDropModeDetector.shared.stop()
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
NearbyConnectionManager.shared.becomeInvisible()
|
NearbyConnectionManager.shared.becomeInvisible()
|
||||||
}
|
}
|
||||||
@@ -225,6 +246,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
// MARK: - Button Actions
|
// MARK: - Button Actions
|
||||||
|
|
||||||
|
@objc private func visibleMenuItemSelected() {
|
||||||
|
showMainWindow(selectedTab: .settings)
|
||||||
|
}
|
||||||
|
|
||||||
@objc func sendClipboard() {
|
@objc func sendClipboard() {
|
||||||
|
|
||||||
let pasteboard = NSPasteboard.general
|
let pasteboard = NSPasteboard.general
|
||||||
@@ -242,6 +267,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
|
|
||||||
@objc func openMainWindow() {
|
@objc func openMainWindow() {
|
||||||
|
showMainWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func showMainWindow(selectedTab: Tab? = nil) {
|
||||||
|
if let selectedTab {
|
||||||
|
welcomeNavigationState.select(selectedTab)
|
||||||
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore {
|
if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore {
|
||||||
runAfter(seconds: 0.2) {
|
runAfter(seconds: 0.2) {
|
||||||
@@ -268,7 +302,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
openPlusScreen: openPlusScreen,
|
openPlusScreen: openPlusScreen,
|
||||||
openAppAdvertisementView: { self.openSheetView(type: .downloadAndroidApp) },
|
openAppAdvertisementView: { self.openSheetView(type: .downloadAndroidApp) },
|
||||||
openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) },
|
openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) },
|
||||||
checkForNetworkIssues: performDeviceToDeviceCheck
|
checkForNetworkIssues: performDeviceToDeviceCheck,
|
||||||
|
navigationState: welcomeNavigationState
|
||||||
)
|
)
|
||||||
|
|
||||||
welcomeWindow = NSWindow(
|
welcomeWindow = NSWindow(
|
||||||
@@ -397,6 +432,58 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupAndroidAirDropModeDetection() {
|
||||||
|
log("[AppDelegate] Setting up Android AirDrop mode detection. authorization=\(BluetoothPermissionManager.shared.authorization.rawValue)")
|
||||||
|
|
||||||
|
bluetoothAuthorizationObserver = BluetoothPermissionManager.shared.$authorization
|
||||||
|
.removeDuplicates()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] authorization in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
log("[AppDelegate] Bluetooth authorization changed for Android AirDrop mode detection: \(authorization.rawValue)")
|
||||||
|
|
||||||
|
if authorization == .allowedAlways {
|
||||||
|
self.startAndroidAirDropModeDetection()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
AndroidAirDropModeDetector.shared.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothPermissionManager.shared.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestBluetoothPermissionForExistingUserIfNeeded() {
|
||||||
|
let bluetoothPermission = BluetoothPermissionManager.shared
|
||||||
|
bluetoothPermission.refresh()
|
||||||
|
|
||||||
|
guard Settings.sharedInstance.appLaunchedBefore, bluetoothPermission.canRequestPermission else { return }
|
||||||
|
|
||||||
|
log("[AppDelegate] Requesting Bluetooth permission for existing user.")
|
||||||
|
|
||||||
|
bluetoothPermission.requestPermission { allowed in
|
||||||
|
log("[AppDelegate] Existing user Bluetooth permission request completed. allowed=\(allowed)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startAndroidAirDropModeDetection() {
|
||||||
|
guard !didShowAndroidAirDropModeAlert else { return }
|
||||||
|
|
||||||
|
AndroidAirDropModeDetector.shared.start { [weak self] _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.showAndroidAirDropModeAlertIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAndroidAirDropModeAlertIfNeeded() {
|
||||||
|
guard !didShowAndroidAirDropModeAlert else { return }
|
||||||
|
|
||||||
|
didShowAndroidAirDropModeAlert = true
|
||||||
|
ErrorAlertHandler.shared.openAlert(type: .AndroidAirDropMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func getDefaultVisibleLabel() -> String {
|
private func getDefaultVisibleLabel() -> String {
|
||||||
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
|
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Smartphone.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 284 KiB |
@@ -11,11 +11,21 @@ import SwiftUI
|
|||||||
let configuration = LUIConfiguration(
|
let configuration = LUIConfiguration(
|
||||||
appName: "QuickDrop",
|
appName: "QuickDrop",
|
||||||
|
|
||||||
plusVersionInfo: GetPlusViewInformation(lifetimeID: "plusversion", monthlyID: "quickdrop.monthly", yearlyID: "quickdrop.yearly", plusViewHeader: isMac() ? "plusview_title" : nil, description: isMac() ? "plusview_description" : "plusview_description_phone", onBoughtPlusVersionAlertMessage: "plusview_success_description", plusVersionFeatures: [], settingsLabelColor: .orange, settingsFooter: "plusview_footer_phone"),
|
plusVersionInfo: GetPlusViewInformation(
|
||||||
|
lifetimeID: "plusversion",
|
||||||
|
monthlyID: "quickdrop.monthly",
|
||||||
|
yearlyID: "quickdrop.yearly",
|
||||||
|
plusViewHeader: isMac() ? "plusview_title" : nil,
|
||||||
|
description: isMac() ? "plusview_description" : "plusview_description_phone",
|
||||||
|
onBoughtPlusVersionAlertMessage: "plusview_success_description",
|
||||||
|
plusVersionFeatures: [],
|
||||||
|
settingsLabelColor: .orange,
|
||||||
|
settingsFooter: "plusview_footer_phone"
|
||||||
|
),
|
||||||
|
|
||||||
hasRoundedFont: false,
|
hasRoundedFont: false,
|
||||||
|
|
||||||
switchedToIAPafterBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
|
switchedToInAppPurchaseWithBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
|
||||||
|
|
||||||
copyrightInformation: licenseText,
|
copyrightInformation: licenseText,
|
||||||
|
|
||||||
@@ -37,5 +47,12 @@ let configuration = LUIConfiguration(
|
|||||||
|
|
||||||
supportsPortraitSplitView: false,
|
supportsPortraitSplitView: false,
|
||||||
|
|
||||||
supportsDarkModeShadow: true
|
supportsDarkModeShadow: false,
|
||||||
|
|
||||||
|
privacyPolicyURL: .privacyPolicyURL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
static let privacyPolicyURL = "https://leonboettger.com/quickdrop-privacy"
|
||||||
|
}
|
||||||
@@ -27,6 +27,472 @@
|
|||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"NSBluetoothAlwaysUsageDescription" : {
|
||||||
|
"comment" : "Privacy - Bluetooth Always Usage Description",
|
||||||
|
"localizations" : {
|
||||||
|
"ar" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ca" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cs" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"da" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"el" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-AU" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-419" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"he" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nb" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-BR" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-PT" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ro" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sv" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"th" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uk" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NSBluetoothPeripheralUsageDescription" : {
|
||||||
|
"comment" : "Privacy - Bluetooth Peripheral Usage Description",
|
||||||
|
"localizations" : {
|
||||||
|
"ar" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ca" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cs" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"da" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"el" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-AU" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-419" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"he" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nb" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-BR" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-PT" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ro" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sv" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"th" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uk" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"NSHumanReadableCopyright" : {
|
"NSHumanReadableCopyright" : {
|
||||||
"comment" : "Copyright (human-readable)",
|
"comment" : "Copyright (human-readable)",
|
||||||
"extractionState" : "extracted_with_value",
|
"extractionState" : "extracted_with_value",
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
//
|
||||||
|
// AndroidAirDropModeDetector.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.06.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreBluetooth
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
struct AndroidAirDropAdvertisement: Equatable {
|
||||||
|
let rssi: Int
|
||||||
|
let manufacturerDataHex: String?
|
||||||
|
let serviceUUIDs: Set<String>
|
||||||
|
let serviceDataHexByUUID: [String: String]
|
||||||
|
let isConnectable: Bool?
|
||||||
|
|
||||||
|
init(
|
||||||
|
rssi: Int,
|
||||||
|
manufacturerDataHex: String?,
|
||||||
|
serviceUUIDs: Set<String>,
|
||||||
|
serviceDataHexByUUID: [String: String] = [:],
|
||||||
|
isConnectable: Bool? = nil
|
||||||
|
) {
|
||||||
|
self.rssi = rssi
|
||||||
|
self.manufacturerDataHex = manufacturerDataHex?.lowercased()
|
||||||
|
self.serviceUUIDs = Set(serviceUUIDs.map { $0.uppercased() })
|
||||||
|
self.serviceDataHexByUUID = Dictionary(
|
||||||
|
uniqueKeysWithValues: serviceDataHexByUUID.map { ($0.key.uppercased(), $0.value.lowercased()) }
|
||||||
|
)
|
||||||
|
self.isConnectable = isConnectable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AndroidAirDropModeObservation: Equatable {
|
||||||
|
let airDropModePacketCount: Int
|
||||||
|
let strongestAirDropModeRSSI: Int?
|
||||||
|
let minimumRSSI: Int
|
||||||
|
let requiredPacketsPerSignal: Int
|
||||||
|
|
||||||
|
var isDetected: Bool {
|
||||||
|
airDropModePacketCount >= requiredPacketsPerSignal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AndroidAirDropModeClassifier {
|
||||||
|
static let defaultMinimumRSSI = -70
|
||||||
|
static let defaultRequiredPacketsPerSignal = 2
|
||||||
|
|
||||||
|
static let airDropModeManufacturerPrefix = "4c000512000000000000000001"
|
||||||
|
static let airDropModeServiceUUID = "FCF1"
|
||||||
|
|
||||||
|
let minimumRSSI: Int
|
||||||
|
let requiredPacketsPerSignal: Int
|
||||||
|
|
||||||
|
private(set) var airDropModePacketCount = 0
|
||||||
|
private(set) var strongestAirDropModeRSSI: Int?
|
||||||
|
|
||||||
|
init(
|
||||||
|
minimumRSSI: Int = Self.defaultMinimumRSSI,
|
||||||
|
requiredPacketsPerSignal: Int = Self.defaultRequiredPacketsPerSignal
|
||||||
|
) {
|
||||||
|
self.minimumRSSI = minimumRSSI
|
||||||
|
self.requiredPacketsPerSignal = requiredPacketsPerSignal
|
||||||
|
}
|
||||||
|
|
||||||
|
var observation: AndroidAirDropModeObservation {
|
||||||
|
AndroidAirDropModeObservation(
|
||||||
|
airDropModePacketCount: airDropModePacketCount,
|
||||||
|
strongestAirDropModeRSSI: strongestAirDropModeRSSI,
|
||||||
|
minimumRSSI: minimumRSSI,
|
||||||
|
requiredPacketsPerSignal: requiredPacketsPerSignal
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func reset() {
|
||||||
|
airDropModePacketCount = 0
|
||||||
|
strongestAirDropModeRSSI = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
mutating func record(_ advertisement: AndroidAirDropAdvertisement) -> AndroidAirDropModeObservation {
|
||||||
|
guard advertisement.rssi >= minimumRSSI else {
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self.isAirDropModeAdvertisement(advertisement) {
|
||||||
|
airDropModePacketCount += 1
|
||||||
|
strongestAirDropModeRSSI = Self.strongerRSSI(strongestAirDropModeRSSI, advertisement.rssi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isAirDropModeAdvertisement(_ advertisement: AndroidAirDropAdvertisement) -> Bool {
|
||||||
|
#if os(iOS)
|
||||||
|
// iOS exposes the AirDrop-compatible Android share-mode service UUID,
|
||||||
|
// but hides the Apple manufacturer payload from third-party apps. The
|
||||||
|
// share sheet emits a non-connectable FCF1 packet without service data;
|
||||||
|
// background AirDrop-compatible discovery emits connectable FCF1 packets
|
||||||
|
// with FCF1 service data.
|
||||||
|
advertisement.serviceUUIDs.contains(airDropModeServiceUUID) &&
|
||||||
|
advertisement.isConnectable == false &&
|
||||||
|
advertisement.serviceDataHexByUUID[airDropModeServiceUUID] == nil
|
||||||
|
#else
|
||||||
|
advertisement.manufacturerDataHex?.hasPrefix(airDropModeManufacturerPrefix) == true &&
|
||||||
|
advertisement.serviceUUIDs.contains(airDropModeServiceUUID)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func strongerRSSI(_ current: Int?, _ candidate: Int) -> Int {
|
||||||
|
guard let current else { return candidate }
|
||||||
|
return max(current, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class AndroidAirDropModeDetector: NSObject, CBCentralManagerDelegate {
|
||||||
|
static let shared = AndroidAirDropModeDetector()
|
||||||
|
static let scanServiceUUIDs = [CBUUID(string: AndroidAirDropModeClassifier.airDropModeServiceUUID)]
|
||||||
|
static let scanOptions: [String: Any] = [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
||||||
|
|
||||||
|
private var centralManager: CBCentralManager?
|
||||||
|
private var classifier = AndroidAirDropModeClassifier()
|
||||||
|
private var onDetected: ((AndroidAirDropModeObservation) -> Void)?
|
||||||
|
private var isScanning = false
|
||||||
|
private var hasDeliveredDetection = false
|
||||||
|
|
||||||
|
func start(onDetected: @escaping (AndroidAirDropModeObservation) -> Void) {
|
||||||
|
guard BluetoothPermissionManager.shared.isAllowed else {
|
||||||
|
log("[AndroidAirDropModeDetector] Bluetooth permission is not granted; skipping scan. authorization=\(BluetoothPermissionManager.shared.authorization.rawValue)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onDetected = onDetected
|
||||||
|
|
||||||
|
guard !hasDeliveredDetection else { return }
|
||||||
|
guard !isScanning else { return }
|
||||||
|
|
||||||
|
log("[AndroidAirDropModeDetector] Starting detector.")
|
||||||
|
|
||||||
|
classifier.reset()
|
||||||
|
|
||||||
|
if centralManager == nil {
|
||||||
|
log("[AndroidAirDropModeDetector] Creating central manager.")
|
||||||
|
centralManager = CBCentralManager(delegate: self, queue: .main)
|
||||||
|
}
|
||||||
|
else if centralManager?.state == .poweredOn {
|
||||||
|
beginScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
log("[AndroidAirDropModeDetector] Stopping detector.")
|
||||||
|
centralManager?.stopScan()
|
||||||
|
classifier.reset()
|
||||||
|
onDetected = nil
|
||||||
|
hasDeliveredDetection = false
|
||||||
|
isScanning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
|
log("[AndroidAirDropModeDetector] Central state changed: \(central.state.rawValue)")
|
||||||
|
|
||||||
|
guard central.state == .poweredOn else {
|
||||||
|
if isScanning {
|
||||||
|
central.stopScan()
|
||||||
|
isScanning = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if onDetected != nil {
|
||||||
|
beginScanning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManager(
|
||||||
|
_ central: CBCentralManager,
|
||||||
|
didDiscover peripheral: CBPeripheral,
|
||||||
|
advertisementData: [String: Any],
|
||||||
|
rssi RSSI: NSNumber
|
||||||
|
) {
|
||||||
|
guard !hasDeliveredDetection else { return }
|
||||||
|
|
||||||
|
let advertisement = Self.advertisement(from: advertisementData, rssi: RSSI.intValue)
|
||||||
|
let observation = classifier.record(advertisement)
|
||||||
|
|
||||||
|
guard observation.isDetected else { return }
|
||||||
|
|
||||||
|
hasDeliveredDetection = true
|
||||||
|
isScanning = false
|
||||||
|
central.stopScan()
|
||||||
|
|
||||||
|
let callback = onDetected
|
||||||
|
onDetected = nil
|
||||||
|
|
||||||
|
log(
|
||||||
|
"[AndroidAirDropModeDetector] Detected Android AirDrop-compatible sharing mode. " +
|
||||||
|
"airDropModePackets=\(observation.airDropModePacketCount), " +
|
||||||
|
"minimumRSSI=\(observation.minimumRSSI)"
|
||||||
|
)
|
||||||
|
|
||||||
|
callback?(observation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginScanning() {
|
||||||
|
guard !isScanning, let centralManager, centralManager.state == .poweredOn else { return }
|
||||||
|
|
||||||
|
let scanTargetDescription = Self.scanServiceUUIDs.map(\.uuidString).joined(separator: ", ")
|
||||||
|
log("[AndroidAirDropModeDetector] BLE scan started for \(scanTargetDescription).")
|
||||||
|
|
||||||
|
// Duplicate delivery is intentional: the classifier waits for repeated
|
||||||
|
// matching packets so one transient advertisement does not trigger the alert.
|
||||||
|
centralManager.scanForPeripherals(
|
||||||
|
withServices: Self.scanServiceUUIDs,
|
||||||
|
options: Self.scanOptions
|
||||||
|
)
|
||||||
|
isScanning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func advertisement(
|
||||||
|
from advertisementData: [String: Any],
|
||||||
|
rssi: Int
|
||||||
|
) -> AndroidAirDropAdvertisement {
|
||||||
|
var serviceUUIDs = Set<String>()
|
||||||
|
var serviceDataHexByUUID: [String: String] = [:]
|
||||||
|
|
||||||
|
[
|
||||||
|
CBAdvertisementDataServiceUUIDsKey,
|
||||||
|
CBAdvertisementDataOverflowServiceUUIDsKey,
|
||||||
|
CBAdvertisementDataSolicitedServiceUUIDsKey
|
||||||
|
].forEach { key in
|
||||||
|
guard let uuids = advertisementData[key] as? [CBUUID] else { return }
|
||||||
|
uuids.forEach { serviceUUIDs.insert($0.uuidString) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID: Data] {
|
||||||
|
serviceData.forEach { uuid, data in
|
||||||
|
serviceUUIDs.insert(uuid.uuidString)
|
||||||
|
serviceDataHexByUUID[uuid.uuidString] = data.androidAirDropModeHexString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AndroidAirDropAdvertisement(
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerDataHex: (advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data)?.androidAirDropModeHexString,
|
||||||
|
serviceUUIDs: serviceUUIDs,
|
||||||
|
serviceDataHexByUUID: serviceDataHexByUUID,
|
||||||
|
isConnectable: advertisementData[CBAdvertisementDataIsConnectable] as? Bool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Data {
|
||||||
|
var androidAirDropModeHexString: String {
|
||||||
|
map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// BluetoothPermissionManager.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 31.05.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreBluetooth
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
#if os(iOS) && !EXTENSION
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
final class BluetoothPermissionManager: NSObject, ObservableObject, CBCentralManagerDelegate {
|
||||||
|
|
||||||
|
static let shared = BluetoothPermissionManager()
|
||||||
|
|
||||||
|
@Published private(set) var authorization: CBManagerAuthorization = CBManager.authorization
|
||||||
|
|
||||||
|
private var centralManager: CBCentralManager?
|
||||||
|
private var completion: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
var isAllowed: Bool {
|
||||||
|
authorization == .allowedAlways
|
||||||
|
}
|
||||||
|
|
||||||
|
var canRequestPermission: Bool {
|
||||||
|
authorization == .notDetermined
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func refresh() {
|
||||||
|
let currentAuthorization = CBManager.authorization
|
||||||
|
guard authorization != currentAuthorization else { return }
|
||||||
|
|
||||||
|
authorization = currentAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestPermission(completion: ((Bool) -> Void)? = nil) {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
guard authorization == .notDetermined else {
|
||||||
|
completion?(isAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.completion = completion
|
||||||
|
centralManager = CBCentralManager(delegate: self, queue: .main)
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
guard authorization != .notDetermined else { return }
|
||||||
|
|
||||||
|
let completion = completion
|
||||||
|
self.completion = nil
|
||||||
|
completion?(isAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openSystemBluetoothSettings() {
|
||||||
|
#if os(iOS) && !EXTENSION
|
||||||
|
openAppSettings()
|
||||||
|
#elseif os(macOS)
|
||||||
|
let urls = [
|
||||||
|
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Bluetooth"),
|
||||||
|
URL(string: "x-apple.systempreferences:com.apple.BluetoothSettings")
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
|
if let url = urls.first {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
//
|
||||||
|
// ClipboardSyncCoordinator.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class ClipboardSyncCoordinator {
|
||||||
|
static let shared = ClipboardSyncCoordinator()
|
||||||
|
|
||||||
|
private let pollInterval: TimeInterval = 1
|
||||||
|
private let transferTimeout: TimeInterval = 75
|
||||||
|
|
||||||
|
private var isStarted = false
|
||||||
|
private var pollTimer: Timer?
|
||||||
|
private var trustStoreObserver: NSObjectProtocol?
|
||||||
|
private var lastObservedChangeCount: Int
|
||||||
|
private var suppressedClipboardText: String?
|
||||||
|
private var startedOwnDiscovery = false
|
||||||
|
private var activeTransfersByFingerprint: [String: ClipboardSyncTransferDelegate] = [:]
|
||||||
|
private var queuedTextByFingerprint: [String: String] = [:]
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
lastObservedChangeCount = NSPasteboard.general.changeCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
guard !isStarted else {
|
||||||
|
refreshPollingState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isStarted = true
|
||||||
|
trustStoreObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: TrustStore.trustedCertificatesDidChange,
|
||||||
|
object: TrustStore.shared,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.refreshPollingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPollingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() {
|
||||||
|
guard isStarted || pollTimer != nil else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Stopping clipboard watcher")
|
||||||
|
isStarted = false
|
||||||
|
if let trustStoreObserver {
|
||||||
|
NotificationCenter.default.removeObserver(trustStoreObserver)
|
||||||
|
self.trustStoreObserver = nil
|
||||||
|
}
|
||||||
|
stopPolling(clearTransfers: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshPollingState() {
|
||||||
|
guard isStarted else { return }
|
||||||
|
|
||||||
|
guard hasTrustedClipboardSyncDevice else {
|
||||||
|
if pollTimer != nil {
|
||||||
|
log("[ClipboardSyncCoordinator] Pausing clipboard watcher because no trusted clipboard sync device is saved")
|
||||||
|
}
|
||||||
|
stopPolling(clearTransfers: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startPollingIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasTrustedClipboardSyncDevice: Bool {
|
||||||
|
TrustStore.shared.trustedCertificates.values.contains {
|
||||||
|
$0.grantedUseCases.contains(.clipboardSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startPollingIfNeeded() {
|
||||||
|
guard pollTimer == nil else { return }
|
||||||
|
|
||||||
|
let manager = NearbyConnectionManager.shared
|
||||||
|
if !manager.isDeviceDiscoveryActive {
|
||||||
|
startedOwnDiscovery = true
|
||||||
|
manager.startDeviceDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
lastObservedChangeCount = NSPasteboard.general.changeCount
|
||||||
|
log("[ClipboardSyncCoordinator] Starting clipboard watcher at changeCount=\(lastObservedChangeCount)")
|
||||||
|
pollTimer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in
|
||||||
|
self?.pollClipboard()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopPolling(clearTransfers: Bool) {
|
||||||
|
pollTimer?.invalidate()
|
||||||
|
pollTimer = nil
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
if clearTransfers {
|
||||||
|
activeTransfersByFingerprint.values.forEach { $0.cancelTimeout() }
|
||||||
|
activeTransfersByFingerprint.removeAll()
|
||||||
|
queuedTextByFingerprint.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard startedOwnDiscovery else { return }
|
||||||
|
startedOwnDiscovery = false
|
||||||
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
|
}
|
||||||
|
|
||||||
|
func suppressNextClipboardSync(for text: String) {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
setSuppressedClipboardText(text)
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
setSuppressedClipboardText(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setSuppressedClipboardText(_ text: String) {
|
||||||
|
log("[ClipboardSyncCoordinator] Suppressing next clipboard sync for incoming text with \(text.utf8.count) bytes")
|
||||||
|
suppressedClipboardText = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pollClipboard() {
|
||||||
|
let pasteboard = NSPasteboard.general
|
||||||
|
let currentChangeCount = pasteboard.changeCount
|
||||||
|
guard currentChangeCount != lastObservedChangeCount else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change detected: \(lastObservedChangeCount) -> \(currentChangeCount)")
|
||||||
|
lastObservedChangeCount = currentChangeCount
|
||||||
|
|
||||||
|
guard let clipboardText = pasteboard.string(forType: .string) else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change ignored because no plain text content is available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !clipboardText.isEmpty else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change ignored because text content is empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if suppressedClipboardText == clipboardText {
|
||||||
|
log("[ClipboardSyncCoordinator] Ignoring clipboard change because it matches an incoming clipboard transfer")
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
suppressedClipboardText = nil
|
||||||
|
|
||||||
|
sendClipboardText(clipboardText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sendClipboardText(_ clipboardText: String) {
|
||||||
|
let targets = nearbyTrustedClipboardTargets()
|
||||||
|
guard !targets.isEmpty else {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard change will not be sent because no trusted clipboard-capable devices are nearby")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Sending clipboard contents to \(targets.count) trusted clipboard-capable device(s)")
|
||||||
|
|
||||||
|
for target in targets {
|
||||||
|
startTransfer(to: target, clipboardText: clipboardText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTransfer(to target: ClipboardSyncTarget, clipboardText: String) {
|
||||||
|
guard activeTransfersByFingerprint[target.fingerprint] == nil else {
|
||||||
|
queuedTextByFingerprint[target.fingerprint] = clipboardText
|
||||||
|
log("[ClipboardSyncCoordinator] Queued latest clipboard text for \(target.logName) because a transfer is already active")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let delegate = ClipboardSyncTransferDelegate(target: target) { [weak self] finishedTarget, finishedDelegate in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.handleTransferCompleted(for: finishedTarget, delegate: finishedDelegate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeTransfersByFingerprint[target.fingerprint] = delegate
|
||||||
|
delegate.startTimeout(after: transferTimeout)
|
||||||
|
|
||||||
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: target.deviceID,
|
||||||
|
delegate: delegate,
|
||||||
|
urls: [],
|
||||||
|
textToSend: clipboardText,
|
||||||
|
isClipboardSyncTransfer: true,
|
||||||
|
receiverAuthenticationPolicy: .trustedReceiver(fingerprint: target.fingerprint)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleTransferCompleted(for target: ClipboardSyncTarget, delegate: ClipboardSyncTransferDelegate) {
|
||||||
|
guard activeTransfersByFingerprint[target.fingerprint] === delegate else {
|
||||||
|
log("[ClipboardSyncCoordinator] Ignoring stale clipboard sync completion for \(target.logName)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activeTransfersByFingerprint.removeValue(forKey: target.fingerprint)
|
||||||
|
|
||||||
|
guard let queuedText = queuedTextByFingerprint.removeValue(forKey: target.fingerprint) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let refreshedTarget = nearbyTrustedClipboardTargets()
|
||||||
|
.first(where: { $0.fingerprint == target.fingerprint }) else {
|
||||||
|
log("[ClipboardSyncCoordinator] Dropping queued clipboard text for \(target.logName) because the device is no longer an eligible target")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Sending queued latest clipboard text to \(refreshedTarget.logName)")
|
||||||
|
startTransfer(to: refreshedTarget, clipboardText: queuedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nearbyTrustedClipboardTargets() -> [ClipboardSyncTarget] {
|
||||||
|
let trustedFingerprints = Set(
|
||||||
|
TrustStore.shared.trustedCertificates.keys.map {
|
||||||
|
$0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var seenFingerprints = Set<String>()
|
||||||
|
|
||||||
|
return NearbyConnectionManager.shared.discoveredDevices()
|
||||||
|
.compactMap { device -> ClipboardSyncTarget? in
|
||||||
|
guard let deviceID = device.id,
|
||||||
|
!deviceID.isEmpty,
|
||||||
|
let fingerprint = device.keyFingerprint,
|
||||||
|
trustedFingerprints.contains(fingerprint),
|
||||||
|
TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: fingerprint),
|
||||||
|
device.supportsClipboardReceive,
|
||||||
|
device.type != .computer,
|
||||||
|
seenFingerprints.insert(fingerprint).inserted else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipboardSyncTarget(
|
||||||
|
device: device,
|
||||||
|
deviceID: deviceID,
|
||||||
|
fingerprint: fingerprint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sorted { lhs, rhs in
|
||||||
|
lhs.logName < rhs.logName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ClipboardSyncTarget {
|
||||||
|
let device: RemoteDeviceInfo
|
||||||
|
let deviceID: String
|
||||||
|
let fingerprint: String
|
||||||
|
|
||||||
|
var logName: String {
|
||||||
|
device.name ?? deviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ClipboardSyncTransferDelegate: OutboundAppDelegate {
|
||||||
|
private let target: ClipboardSyncTarget
|
||||||
|
private let onComplete: (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void
|
||||||
|
private var isCompleted = false
|
||||||
|
private var timeoutWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
init(target: ClipboardSyncTarget, onComplete: @escaping (ClipboardSyncTarget, ClipboardSyncTransferDelegate) -> Void) {
|
||||||
|
self.target = target
|
||||||
|
self.onComplete = onComplete
|
||||||
|
}
|
||||||
|
|
||||||
|
func startTimeout(after timeout: TimeInterval) {
|
||||||
|
let timeoutWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self, !self.isCompleted else { return }
|
||||||
|
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(self.target.logName) timed out")
|
||||||
|
NearbyConnectionManager.shared.cancelTransfer(id: self.target.deviceID)
|
||||||
|
self.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timeoutWorkItem = timeoutWorkItem
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: timeoutWorkItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelTimeout() {
|
||||||
|
timeoutWorkItem?.cancel()
|
||||||
|
timeoutWorkItem = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDevice(device _: RemoteDeviceInfo) {}
|
||||||
|
|
||||||
|
func removeDevice(id _: String) {}
|
||||||
|
|
||||||
|
func startTransferWithQrCode(device _: RemoteDeviceInfo) {}
|
||||||
|
|
||||||
|
func connectionWasEstablished(pinCode _: String) {}
|
||||||
|
|
||||||
|
func transferAccepted() {}
|
||||||
|
|
||||||
|
func transferProgress(progress _: Double) {}
|
||||||
|
|
||||||
|
func connectionFailed(error: Error) {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) failed: \(error.localizedDescription)")
|
||||||
|
complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
func transferFinished() {
|
||||||
|
log("[ClipboardSyncCoordinator] Clipboard sync to \(target.logName) finished")
|
||||||
|
complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func complete() {
|
||||||
|
guard !isCompleted else { return }
|
||||||
|
isCompleted = true
|
||||||
|
cancelTimeout()
|
||||||
|
onComplete(target, self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
//
|
||||||
|
// ClipboardSyncNotificationPresenter.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(macOS) && !EXTENSION
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UserNotifications
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class ClipboardSyncNotificationPresenter {
|
||||||
|
|
||||||
|
static let shared = ClipboardSyncNotificationPresenter()
|
||||||
|
static let notificationIdentifier = "quickdrop-clipboard-sync"
|
||||||
|
private static let notificationIdentifierPrefix = "\(notificationIdentifier)-"
|
||||||
|
|
||||||
|
private let center = UNUserNotificationCenter.current()
|
||||||
|
private let queue = DispatchQueue(label: "ClipboardSyncNotificationPresenter")
|
||||||
|
private var pendingNotifications: [(String, String?)] = []
|
||||||
|
private var authorizationRequestInFlight = false
|
||||||
|
private var postedNotificationIdentifiers: Set<String> = []
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
static func isClipboardSyncNotification(_ notification: UNNotification) -> Bool {
|
||||||
|
notification.request.identifier == notificationIdentifier
|
||||||
|
|| notification.request.identifier.hasPrefix(notificationIdentifierPrefix)
|
||||||
|
|| notification.request.content.threadIdentifier == notificationIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func present(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
queue.async {
|
||||||
|
self.postNotificationIfAuthorized(
|
||||||
|
clipboardText: clipboardText,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postNotificationIfAuthorized(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
center.getNotificationSettings { settings in
|
||||||
|
self.queue.async {
|
||||||
|
self.handleAuthorizationStatus(
|
||||||
|
settings.authorizationStatus,
|
||||||
|
clipboardText: clipboardText,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthorizationStatus(
|
||||||
|
_ status: UNAuthorizationStatus,
|
||||||
|
clipboardText: String,
|
||||||
|
senderDeviceName: String?
|
||||||
|
) {
|
||||||
|
switch status {
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
post(clipboardText: clipboardText, senderDeviceName: senderDeviceName)
|
||||||
|
case .notDetermined:
|
||||||
|
pendingNotifications.append((clipboardText, senderDeviceName))
|
||||||
|
requestAuthorizationIfNeeded()
|
||||||
|
case .denied:
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Notifications are denied in system settings; cannot show clipboard sync notification.")
|
||||||
|
@unknown default:
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Unknown notification authorization status; skipping clipboard sync notification.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAuthorizationIfNeeded() {
|
||||||
|
if authorizationRequestInFlight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationRequestInFlight = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.center.requestAuthorization(options: [.alert, .badge]) { granted, error in
|
||||||
|
self.queue.async {
|
||||||
|
self.authorizationRequestInFlight = false
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard granted else {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Authorization request not granted; dropping queued clipboard sync notifications.")
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let queued = self.pendingNotifications
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
for (queuedText, queuedSender) in queued {
|
||||||
|
self.post(clipboardText: queuedText, senderDeviceName: queuedSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post(clipboardText: String, senderDeviceName: String?) {
|
||||||
|
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "CopiedToClipboard".localized()
|
||||||
|
content.body = preview(for: clipboardText)
|
||||||
|
content.subtitle = senderName?.isEmpty == false ? senderName! : "QuickDrop"
|
||||||
|
content.threadIdentifier = Self.notificationIdentifier
|
||||||
|
content.interruptionLevel = .active
|
||||||
|
|
||||||
|
let identifier = Self.makeNotificationIdentifier()
|
||||||
|
var identifiersToRemove = postedNotificationIdentifiers
|
||||||
|
identifiersToRemove.insert(Self.notificationIdentifier)
|
||||||
|
postedNotificationIdentifiers = [identifier]
|
||||||
|
center.removePendingNotificationRequests(withIdentifiers: Array(identifiersToRemove))
|
||||||
|
center.removeDeliveredNotifications(withIdentifiers: Array(identifiersToRemove))
|
||||||
|
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||||
|
center.add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Could not post notification: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
log("[ClipboardSyncNotificationPresenter] Posted clipboard sync notification (\(identifier)).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preview(for clipboardText: String) -> String {
|
||||||
|
clipboardText
|
||||||
|
.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeNotificationIdentifier() -> String {
|
||||||
|
"\(notificationIdentifierPrefix)\(UUID().uuidString)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -27,15 +27,35 @@ class ErrorAlertHandler {
|
|||||||
private var firewallAlertWindow: NSWindow?
|
private var firewallAlertWindow: NSWindow?
|
||||||
private var apIsolationAlertWindow: NSWindow?
|
private var apIsolationAlertWindow: NSWindow?
|
||||||
private var networkFilterAlertWindow: NSWindow?
|
private var networkFilterAlertWindow: NSWindow?
|
||||||
|
private var androidAirDropModeAlertWindow: NSWindow?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
func showErrorAlert(for deviceName: String, error: Error) {
|
/// Resource key for the recoverable-error suffix ("if this keeps happening…") appended to most
|
||||||
|
/// transfer errors.
|
||||||
|
///
|
||||||
|
/// `Error.FixInstructions` suggests downloading the QuickDrop Android app from Google Play, which
|
||||||
|
/// is pointless for a peer that already runs QuickDrop. For such a peer (`isQuickDropPeer == true`)
|
||||||
|
/// the `Error.FixInstructionsAppInstalled` variant — the same advice without the download
|
||||||
|
/// suggestion — is used instead.
|
||||||
|
static func fixInstructionsKey(isQuickDropPeer: Bool) -> String {
|
||||||
|
isQuickDropPeer ? "Error.FixInstructionsAppInstalled" : "Error.FixInstructions"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Presents the transfer-error alert for `deviceName`.
|
||||||
|
///
|
||||||
|
/// - Parameter isQuickDropPeer: whether the other device runs the QuickDrop app. When `true`, the
|
||||||
|
/// "download the app from Google Play" suggestion is omitted from the fix instructions (see
|
||||||
|
/// `fixInstructionsKey(isQuickDropPeer:)`). Defaults to `false` so callers without device context
|
||||||
|
/// keep the full hint.
|
||||||
|
/// - Parameter completion: called after the macOS transfer alert is dismissed. For issue-specific
|
||||||
|
/// alerts and iOS alerts, it is called after presentation.
|
||||||
|
func showErrorAlert(for deviceName: String, error: Error, isQuickDropPeer: Bool = false, completion: (() -> Void)? = nil) {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var description = ""
|
var description = ""
|
||||||
let fixInstructions = " " + "Error.FixInstructions".localized()
|
let fixInstructions = " " + Self.fixInstructionsKey(isQuickDropPeer: isQuickDropPeer).localized()
|
||||||
if let ne = (error as? NearbyError) {
|
if let ne = (error as? NearbyError) {
|
||||||
switch ne {
|
switch ne {
|
||||||
case .inputOutput:
|
case .inputOutput:
|
||||||
@@ -45,6 +65,7 @@ class ErrorAlertHandler {
|
|||||||
case .packetFilterError:
|
case .packetFilterError:
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
openAlert(type: .NetworkFilter)
|
openAlert(type: .NetworkFilter)
|
||||||
|
completion?()
|
||||||
return
|
return
|
||||||
#else
|
#else
|
||||||
description = error.localizedDescription
|
description = error.localizedDescription
|
||||||
@@ -52,6 +73,7 @@ class ErrorAlertHandler {
|
|||||||
case .firewallError:
|
case .firewallError:
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
openAlert(type: .Firewall)
|
openAlert(type: .Firewall)
|
||||||
|
completion?()
|
||||||
return
|
return
|
||||||
#else
|
#else
|
||||||
description = error.localizedDescription
|
description = error.localizedDescription
|
||||||
@@ -77,6 +99,7 @@ class ErrorAlertHandler {
|
|||||||
// Prevent multiple alerts at the same time
|
// Prevent multiple alerts at the same time
|
||||||
if self.isAlertShown {
|
if self.isAlertShown {
|
||||||
log("Skipping alert for error \(error.localizedDescription) because one is already shown")
|
log("Skipping alert for error \(error.localizedDescription) because one is already shown")
|
||||||
|
completion?()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -88,31 +111,14 @@ class ErrorAlertHandler {
|
|||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
||||||
let primaryButtonTitle = "InformDeveloper".localized()
|
// Shown modelessly (the native NSAlert panel via makeKeyAndOrderFront) rather than
|
||||||
let secondaryButtonTitle = "CloseAlert".localized()
|
// runModal(), which blocks the main run loop and would freeze the next incoming transfer's
|
||||||
|
// consent prompt until dismissed. Same window runModal() uses, so it looks identical.
|
||||||
self.isAlertShown = true
|
self.isAlertShown = true
|
||||||
let alert = NSAlert()
|
presentTransferErrorAlert(title: title, description: description, completion: completion)
|
||||||
alert.alertStyle = .critical
|
|
||||||
alert.messageText = title
|
|
||||||
alert.informativeText = description
|
|
||||||
alert.addButton(withTitle: primaryButtonTitle)
|
|
||||||
alert.addButton(withTitle: secondaryButtonTitle)
|
|
||||||
|
|
||||||
let result = alert.runModal()
|
|
||||||
self.isAlertShown = false
|
|
||||||
|
|
||||||
if result == .alertFirstButtonReturn {
|
|
||||||
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
|
||||||
if let url = URL(string: "quickdrop://sendLog") {
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
showAlert(title: title, message: description)
|
showAlert(title: title, message: description)
|
||||||
|
completion?()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +130,11 @@ class ErrorAlertHandler {
|
|||||||
AudioManager.playErrorSound()
|
AudioManager.playErrorSound()
|
||||||
|
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
|
if let existingWindow = alertWindow(for: type), existingWindow.isVisible {
|
||||||
|
bringAlertWindowToFront(existingWindow)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create an NSWindow to host the SwiftUI view
|
// Create an NSWindow to host the SwiftUI view
|
||||||
let alertWindow = NSWindow(
|
let alertWindow = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
|
contentRect: NSRect(x: 0, y: 0, width: issueViewWidth, height: issueViewHeight),
|
||||||
@@ -142,6 +153,9 @@ class ErrorAlertHandler {
|
|||||||
case .Firewall:
|
case .Firewall:
|
||||||
firewallAlertWindow = alertWindow
|
firewallAlertWindow = alertWindow
|
||||||
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
|
alertWindow.contentView = NSHostingView(rootView: FirewallIssueView())
|
||||||
|
case .AndroidAirDropMode:
|
||||||
|
androidAirDropModeAlertWindow = alertWindow
|
||||||
|
alertWindow.contentView = NSHostingView(rootView: AndroidAirDropModeIssueView())
|
||||||
}
|
}
|
||||||
|
|
||||||
alertWindow.title = "QuickDrop"
|
alertWindow.title = "QuickDrop"
|
||||||
@@ -149,9 +163,8 @@ class ErrorAlertHandler {
|
|||||||
alertWindow.isReleasedWhenClosed = false
|
alertWindow.isReleasedWhenClosed = false
|
||||||
alertWindow.setFrameAutosaveName(type.rawValue)
|
alertWindow.setFrameAutosaveName(type.rawValue)
|
||||||
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
alertWindow.makeKeyAndOrderFront(nil)
|
|
||||||
alertWindow.level = .normal
|
alertWindow.level = .normal
|
||||||
|
bringAlertWindowToFront(alertWindow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +176,68 @@ class ErrorAlertHandler {
|
|||||||
self.apIsolationAlertWindow = nil
|
self.apIsolationAlertWindow = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func alertWindow(for type: AlertType) -> NSWindow? {
|
||||||
|
switch type {
|
||||||
|
case .ApIsolation:
|
||||||
|
return apIsolationAlertWindow
|
||||||
|
case .NetworkFilter:
|
||||||
|
return networkFilterAlertWindow
|
||||||
|
case .Firewall:
|
||||||
|
return firewallAlertWindow
|
||||||
|
case .AndroidAirDropMode:
|
||||||
|
return androidAirDropModeAlertWindow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bringAlertWindowToFront(_ window: NSWindow) {
|
||||||
|
// Keep the app a menu-bar (.accessory) app: do NOT switch to .regular,
|
||||||
|
// which would add a dock icon that lingers after the alert closes.
|
||||||
|
// orderFrontRegardless() brings the window to the front even though the
|
||||||
|
// app stays inactive in the background.
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Shows the transfer-error alert as the native `NSAlert` panel, but presented modelessly
|
||||||
|
/// (`makeKeyAndOrderFront`) instead of `runModal()`, so it does NOT block the main run loop — an
|
||||||
|
/// incoming transfer's consent prompt can still appear while it's on screen. It's the same
|
||||||
|
/// `_NSAlertPanel` window `runModal()` would show, so it looks identical; we just route the button
|
||||||
|
/// clicks ourselves (there's no modal session to end) and reset state on dismissal.
|
||||||
|
private func presentTransferErrorAlert(title: String, description: String, completion: (() -> Void)? = nil) {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .critical
|
||||||
|
alert.messageText = title
|
||||||
|
alert.informativeText = description
|
||||||
|
alert.addButton(withTitle: "InformDeveloper".localized()) // tag == .alertFirstButtonReturn
|
||||||
|
alert.addButton(withTitle: "CloseAlert".localized()) // tag == .alertSecondButtonReturn
|
||||||
|
alert.buttons.first?.keyEquivalent = "\r" // default button: Return
|
||||||
|
if alert.buttons.count > 1 { alert.buttons[1].keyEquivalent = "\u{1b}" } // Close: Escape
|
||||||
|
|
||||||
|
ModelessAlertPresenter.present(alert) { [weak self] response in
|
||||||
|
self?.isAlertShown = false
|
||||||
|
if response == .alertFirstButtonReturn {
|
||||||
|
Self.presentLogUpload()
|
||||||
|
}
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// The "Inform developer" action: from the share extension, hand off to the host app via the
|
||||||
|
/// `quickdrop://sendLog` URL; otherwise upload logs directly. Mirrors the previous alert's primary
|
||||||
|
/// button.
|
||||||
|
private static func presentLogUpload() {
|
||||||
|
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||||
|
if let url = URL(string: "quickdrop://sendLog") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogExportPresenter.showUploadLogsAlert(openSupportMailAfterUpload: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,4 +246,5 @@ enum AlertType: String {
|
|||||||
case ApIsolation
|
case ApIsolation
|
||||||
case NetworkFilter
|
case NetworkFilter
|
||||||
case Firewall
|
case Firewall
|
||||||
|
case AndroidAirDropMode
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// ModelessAlertPresenter.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 30.06.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
/// Presents an `NSAlert` as its own modeless panel (non-blocking) instead of `runModal()`. It's the
|
||||||
|
/// exact `_NSAlertPanel` window `runModal()` would show, so it looks identical — but it does not spin a
|
||||||
|
/// nested modal run loop, so the app keeps working (e.g. can still receive the next transfer) while the
|
||||||
|
/// alert is on screen.
|
||||||
|
///
|
||||||
|
/// Use this in place of `alert.runModal()` for any alert that may appear while the app is busy. The
|
||||||
|
/// clicked button is reported to `completion` as the `NSApplication.ModalResponse` that `runModal()`
|
||||||
|
/// would have returned (first button = `.alertFirstButtonReturn`, second = `.alertSecondButtonReturn`,
|
||||||
|
/// …). Must be called on the main thread.
|
||||||
|
final class ModelessAlertPresenter: NSObject {
|
||||||
|
|
||||||
|
/// Keeps each live presenter — and thus its `NSAlert` and panel — retained while on screen; the
|
||||||
|
/// entry is removed when the alert is dismissed. `NSButton.target` is weak and the panel is owned by
|
||||||
|
/// the `NSAlert`, so without this the alert would deallocate immediately.
|
||||||
|
private static var active: Set<ModelessAlertPresenter> = []
|
||||||
|
|
||||||
|
private let alert: NSAlert
|
||||||
|
private let completion: (NSApplication.ModalResponse) -> Void
|
||||||
|
|
||||||
|
private init(alert: NSAlert, completion: @escaping (NSApplication.ModalResponse) -> Void) {
|
||||||
|
self.alert = alert
|
||||||
|
self.completion = completion
|
||||||
|
}
|
||||||
|
|
||||||
|
static func present(_ alert: NSAlert,
|
||||||
|
completion: @escaping (NSApplication.ModalResponse) -> Void = { _ in }) {
|
||||||
|
|
||||||
|
if let window = NSApp.mainWindow ?? NSApp.keyWindow {
|
||||||
|
alert.beginSheetModal(for: window, completionHandler: completion)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let presenter = ModelessAlertPresenter(alert: alert, completion: completion)
|
||||||
|
active.insert(presenter)
|
||||||
|
presenter.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func show() {
|
||||||
|
alert.layout()
|
||||||
|
|
||||||
|
// runModal() makes the default button respond to Return; modelessly we set it ourselves — but
|
||||||
|
// only if the caller specified no key equivalents, so explicit ones (e.g. Escape) are kept.
|
||||||
|
if alert.buttons.allSatisfy({ $0.keyEquivalent.isEmpty }) {
|
||||||
|
alert.buttons.first?.keyEquivalent = "\r"
|
||||||
|
}
|
||||||
|
|
||||||
|
for button in alert.buttons {
|
||||||
|
button.target = self
|
||||||
|
button.action = #selector(buttonClicked(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = alert.window
|
||||||
|
window.center()
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.orderFrontRegardless()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func buttonClicked(_ sender: NSButton) {
|
||||||
|
let response = NSApplication.ModalResponse(rawValue: sender.tag)
|
||||||
|
alert.window.orderOut(nil)
|
||||||
|
// Defer teardown so the alert and its panel aren't deallocated while this button's action is
|
||||||
|
// still on the stack.
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.completion(response)
|
||||||
|
ModelessAlertPresenter.active.remove(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ func sendToSharingService(items: [Any]) {
|
|||||||
alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized()
|
alert.informativeText = "ShareExtensionDisabledDescriptionLegacy".localized()
|
||||||
}
|
}
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
alert.runModal()
|
ModelessAlertPresenter.present(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ struct IntroductionView: View {
|
|||||||
static let height: CGFloat = 540
|
static let height: CGFloat = 540
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
@ObservedObject private var bluetooth = BluetoothPermissionManager.shared
|
||||||
@State private var currentPage: IntroductionPage = .splash
|
@State private var currentPage: IntroductionPage = .splash
|
||||||
|
|
||||||
@State private var pollingPage: IntroductionPage? = nil
|
@State private var pollingPage: IntroductionPage? = nil
|
||||||
@@ -120,6 +122,10 @@ struct IntroductionView: View {
|
|||||||
.onChange(of: currentPage) { newValue in
|
.onChange(of: currentPage) { newValue in
|
||||||
pageDidChange(to: newValue)
|
pageDidChange(to: newValue)
|
||||||
}
|
}
|
||||||
|
.onChange(of: scenePhase) { newValue in
|
||||||
|
guard newValue == .active, currentPage == .bluetoothAccess else { return }
|
||||||
|
refreshBluetoothPermissionAndAdvanceIfAllowed()
|
||||||
|
}
|
||||||
.task(id: pollingPage) {
|
.task(id: pollingPage) {
|
||||||
guard let page = pollingPage else { return }
|
guard let page = pollingPage else { return }
|
||||||
|
|
||||||
@@ -181,6 +187,11 @@ struct IntroductionView: View {
|
|||||||
|
|
||||||
|
|
||||||
private func continueTapped() {
|
private func continueTapped() {
|
||||||
|
if currentPage == .bluetoothAccess {
|
||||||
|
continueBluetoothPermission()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// If already allowed, advance immediately
|
// If already allowed, advance immediately
|
||||||
if currentPage.canContinue() {
|
if currentPage.canContinue() {
|
||||||
advance()
|
advance()
|
||||||
@@ -197,6 +208,50 @@ struct IntroductionView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func continueBluetoothPermission() {
|
||||||
|
bluetooth.refresh()
|
||||||
|
|
||||||
|
if bluetooth.isAllowed {
|
||||||
|
advance()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pollingPage = .bluetoothAccess
|
||||||
|
|
||||||
|
guard bluetooth.canRequestPermission else {
|
||||||
|
bluetooth.openSystemBluetoothSettings()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bluetooth.requestPermission { allowed in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard currentPage == .bluetoothAccess else {
|
||||||
|
stopPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowed {
|
||||||
|
stopPolling()
|
||||||
|
advance()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pollingPage = .bluetoothAccess
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func refreshBluetoothPermissionAndAdvanceIfAllowed() {
|
||||||
|
bluetooth.refresh()
|
||||||
|
|
||||||
|
guard currentPage == .bluetoothAccess, bluetooth.isAllowed else { return }
|
||||||
|
|
||||||
|
stopPolling()
|
||||||
|
advance()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func advance() {
|
private func advance() {
|
||||||
|
|
||||||
if currentPage == .localNetworkAccess {
|
if currentPage == .localNetworkAccess {
|
||||||
@@ -223,6 +278,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
case splash
|
case splash
|
||||||
case noWifi
|
case noWifi
|
||||||
case localNetworkAccess
|
case localNetworkAccess
|
||||||
|
case bluetoothAccess
|
||||||
case enableShareExtension
|
case enableShareExtension
|
||||||
case finished
|
case finished
|
||||||
|
|
||||||
@@ -234,6 +290,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
return "introduction_no_wifi"
|
return "introduction_no_wifi"
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
return "introduction_local_network_access"
|
return "introduction_local_network_access"
|
||||||
|
case .bluetoothAccess:
|
||||||
|
return "BluetoothPermissionRequiredTitle"
|
||||||
case .enableShareExtension:
|
case .enableShareExtension:
|
||||||
return "introduction_enable_share_extension"
|
return "introduction_enable_share_extension"
|
||||||
case .finished:
|
case .finished:
|
||||||
@@ -250,6 +308,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
return "introduction_no_wifi_description"
|
return "introduction_no_wifi_description"
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
return "introduction_local_network_access_description".localized(with: "introduction_continue".localized())
|
return "introduction_local_network_access_description".localized(with: "introduction_continue".localized())
|
||||||
|
case .bluetoothAccess:
|
||||||
|
return "BluetoothPermissionRequiredSubtitle"
|
||||||
case .enableShareExtension:
|
case .enableShareExtension:
|
||||||
if #available(macOS 13.0, *) {
|
if #available(macOS 13.0, *) {
|
||||||
return "introduction_enable_share_extension_description".localized(with: "EnableQuickDropExtension".localized())
|
return "introduction_enable_share_extension_description".localized(with: "EnableQuickDropExtension".localized())
|
||||||
@@ -267,6 +327,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .noWifi:
|
case .noWifi:
|
||||||
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message")
|
return .init(action: presentNextPage, warningTitle: "introduction_no_wifi_skip_title", warningMessage: "introduction_no_wifi_skip_message")
|
||||||
|
case .bluetoothAccess:
|
||||||
|
return .init(action: { .enableShareExtension }, warningTitle: "BluetoothPermissionSkipAlertTitle", warningMessage: "BluetoothPermissionSkipAlertMessage")
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -290,6 +352,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
case .noWifi:
|
case .noWifi:
|
||||||
return firstPermissionPane()
|
return firstPermissionPane()
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
|
return .bluetoothAccess
|
||||||
|
case .bluetoothAccess:
|
||||||
return .enableShareExtension
|
return .enableShareExtension
|
||||||
case .enableShareExtension:
|
case .enableShareExtension:
|
||||||
return .finished
|
return .finished
|
||||||
@@ -304,7 +368,7 @@ enum IntroductionPage: CaseIterable {
|
|||||||
// Local network access is requested first on macOS 15+
|
// Local network access is requested first on macOS 15+
|
||||||
return .localNetworkAccess
|
return .localNetworkAccess
|
||||||
}
|
}
|
||||||
return .enableShareExtension
|
return .bluetoothAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -312,6 +376,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
switch self {
|
switch self {
|
||||||
case .noWifi:
|
case .noWifi:
|
||||||
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
|
return NearbyConnectionManager.shared.isConnectedToLocalNetwork
|
||||||
|
case .bluetoothAccess:
|
||||||
|
return BluetoothPermissionManager.shared.isAllowed
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -332,6 +398,8 @@ enum IntroductionPage: CaseIterable {
|
|||||||
imageView(for: "wifi.slash", with: height)
|
imageView(for: "wifi.slash", with: height)
|
||||||
case .localNetworkAccess:
|
case .localNetworkAccess:
|
||||||
imageView(for: "network", with: height)
|
imageView(for: "network", with: height)
|
||||||
|
case .bluetoothAccess:
|
||||||
|
imageView(for: "antenna.radiowaves.left.and.right", with: height)
|
||||||
case .enableShareExtension:
|
case .enableShareExtension:
|
||||||
imageView(for: "square.and.arrow.up", with: height)
|
imageView(for: "square.and.arrow.up", with: height)
|
||||||
case .finished:
|
case .finished:
|
||||||
@@ -351,7 +419,6 @@ enum IntroductionPage: CaseIterable {
|
|||||||
.padding(.top, height * 0.13)
|
.padding(.top, height * 0.13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct SkipAction: Identifiable {
|
struct SkipAction: Identifiable {
|
||||||
let action: () -> IntroductionPage?
|
let action: () -> IntroductionPage?
|
||||||
let warningTitle: String
|
let warningTitle: String
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// AndroidAirDropModeIssueView.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.06.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AndroidAirDropModeIssueView: View {
|
||||||
|
var body: some View {
|
||||||
|
IssueView(
|
||||||
|
image: .smartphone,
|
||||||
|
header: "AndroidAirDropModeHeader".localized(),
|
||||||
|
description: "AndroidAirDropModeDescription".localized()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AndroidAirDropModeIssueView()
|
||||||
|
.frame(width: issueViewWidth, height: issueViewHeight)
|
||||||
|
}
|
||||||
@@ -71,7 +71,7 @@ func openRouterSettingsPage() async {
|
|||||||
alert.informativeText = "ApIsolationFindRouterFailedDescription".localized()
|
alert.informativeText = "ApIsolationFindRouterFailedDescription".localized()
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
alert.runModal()
|
ModelessAlertPresenter.present(alert)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ struct IssueView: View {
|
|||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NetworkFilterIssueView()
|
IssueView(image: .smartphone, header: "Header", description: "Description")
|
||||||
.frame(width: 600, height: 400)
|
.frame(width: 600, height: 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,33 +1,39 @@
|
|||||||
//
|
//
|
||||||
// NotificationSyncQrSheetManager.swift
|
// PairingQrSheetManager.swift
|
||||||
// QuickDrop
|
// QuickDrop
|
||||||
//
|
//
|
||||||
// Created by Codex on 2026-03-15.
|
// Created by Leon Böttger on 2026-03-15.
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate {
|
final class PairingQrSheetManager: NSObject, NSWindowDelegate {
|
||||||
static let shared = NotificationSyncQrSheetManager()
|
static let shared = PairingQrSheetManager()
|
||||||
|
|
||||||
private var alert: NSAlert?
|
private var alert: NSAlert?
|
||||||
private weak var alertWindow: NSWindow?
|
private weak var alertWindow: NSWindow?
|
||||||
|
|
||||||
func open(token: String, receiverFingerprint: String, deviceName: String) {
|
func open(
|
||||||
|
token: String,
|
||||||
|
receiverFingerprint: String,
|
||||||
|
deviceName: String,
|
||||||
|
useCase: PairingUseCase
|
||||||
|
) {
|
||||||
guard alert == nil else { return }
|
guard alert == nil else { return }
|
||||||
guard let qrImage = NotificationSyncPairingToken.makeQrImage(
|
guard let qrImage = PairingToken.makeQrImage(
|
||||||
token: token,
|
token: token,
|
||||||
receiverFingerprint: receiverFingerprint
|
receiverFingerprint: receiverFingerprint,
|
||||||
|
useCase: useCase
|
||||||
) else { return }
|
) else { return }
|
||||||
|
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "NotificationSyncQrPairingTitle".localized()
|
alert.messageText = "PairingQrSheetTitle".localized()
|
||||||
alert.informativeText = "NotificationSyncQrPairingDescription".localized(with: deviceName)
|
alert.informativeText = "PairingQrSheetDescription".localized(with: deviceName)
|
||||||
alert.addButton(withTitle: "Cancel".localized())
|
alert.addButton(withTitle: "Cancel".localized())
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
|
|
||||||
let accessoryView = NSHostingView(rootView: NotificationSyncQrAccessoryView(qrImage: qrImage))
|
let accessoryView = NSHostingView(rootView: PairingQrAccessoryView(qrImage: qrImage))
|
||||||
accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180)
|
accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180)
|
||||||
alert.accessoryView = accessoryView
|
alert.accessoryView = accessoryView
|
||||||
alert.layout()
|
alert.layout()
|
||||||
@@ -71,7 +77,7 @@ final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct NotificationSyncQrAccessoryView: View {
|
private struct PairingQrAccessoryView: View {
|
||||||
let qrImage: Image
|
let qrImage: Image
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -11,10 +11,14 @@ import LUI
|
|||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
let openPlus: () -> Void
|
||||||
|
|
||||||
@ObservedObject var settings = Settings.sharedInstance
|
@ObservedObject var settings = Settings.sharedInstance
|
||||||
@ObservedObject var connectionManager = NearbyConnectionManager.shared
|
@ObservedObject var connectionManager = NearbyConnectionManager.shared
|
||||||
|
@ObservedObject var iapManager = IAPManager.sharedInstance
|
||||||
|
|
||||||
@State private var isChangeDeviceNameAlertPresented = false
|
@State private var isChangeDeviceNameAlertPresented = false
|
||||||
|
@State private var licenseWindow: NSWindow?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@@ -68,18 +72,34 @@ struct SettingsView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|
||||||
Button {
|
HStack(spacing: 30) {
|
||||||
|
SettingsFooterButton("ManageTrustedDevices") {
|
||||||
openTrustedDevicesWindow()
|
openTrustedDevicesWindow()
|
||||||
} label: {
|
}
|
||||||
Text("ManageTrustedDevices")
|
|
||||||
.underline()
|
SettingsFooterButton("Acknowledgements") {
|
||||||
.font(.subheadline)
|
licenseWindow = openLicenseWindow()
|
||||||
.opacity(0.5)
|
}
|
||||||
|
|
||||||
|
#if !GITHUB
|
||||||
|
if !iapManager.plusVersionState {
|
||||||
|
SettingsFooterButton("SupportQuickDrop") {
|
||||||
|
openPlus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
SettingsFooterButton("ResetAllSettings") {
|
||||||
|
IAPManager.sharedInstance.plusVersionState = false
|
||||||
|
Settings.sharedInstance.deleteAllUserDefaults()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 15)
|
.padding(.vertical, 15)
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +151,7 @@ struct SettingsView: View {
|
|||||||
alert.informativeText = "FolderSelectionFailedMessage".localized()
|
alert.informativeText = "FolderSelectionFailedMessage".localized()
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
alert.runModal()
|
ModelessAlertPresenter.present(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,7 +214,31 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
struct SettingsView_Previews: PreviewProvider {
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SettingsView()
|
SettingsView(openPlus: {})
|
||||||
.frame(width: 800, height: 500)
|
.frame(width: 800, height: 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private struct SettingsFooterButton: View {
|
||||||
|
let title: String
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
init(_ title: String, action: @escaping () -> Void) {
|
||||||
|
self.title = title
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
action()
|
||||||
|
} label: {
|
||||||
|
Text(title.localized())
|
||||||
|
.underline()
|
||||||
|
.font(.subheadline)
|
||||||
|
.lineLimit(1)
|
||||||
|
.opacity(0.5)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,10 +13,12 @@ let toastViewSize = CGSize(width: 340, height: 65)
|
|||||||
let toastCornerRadius: CGFloat = 22
|
let toastCornerRadius: CGFloat = 22
|
||||||
let toastTrailingPadding: CGFloat = 20
|
let toastTrailingPadding: CGFloat = 20
|
||||||
let toastSlideDistance: CGFloat = 420
|
let toastSlideDistance: CGFloat = 420
|
||||||
|
let toastSlideAnimationDuration: TimeInterval = 0.5
|
||||||
|
|
||||||
struct QuickDropToastView: View {
|
struct QuickDropToastView: View {
|
||||||
@ObservedObject var settings = Settings.sharedInstance
|
@ObservedObject var settings = Settings.sharedInstance
|
||||||
@ObservedObject var receiveModel: ReceiveModel
|
@ObservedObject var receiveModel: ReceiveModel
|
||||||
|
let animateProgressVisibility: Bool
|
||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@@ -28,9 +30,11 @@ struct QuickDropToastView: View {
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
receiveModel: ReceiveModel,
|
receiveModel: ReceiveModel,
|
||||||
|
animateProgressVisibility: Bool = true,
|
||||||
onCancel: @escaping () -> Void = {}
|
onCancel: @escaping () -> Void = {}
|
||||||
) {
|
) {
|
||||||
self.receiveModel = receiveModel
|
self.receiveModel = receiveModel
|
||||||
|
self.animateProgressVisibility = animateProgressVisibility
|
||||||
self.onCancel = onCancel
|
self.onCancel = onCancel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +138,12 @@ struct QuickDropToastView: View {
|
|||||||
CapsuleProgress(value: progress)
|
CapsuleProgress(value: progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut, value: receiveModel.progress == nil)
|
.animation(
|
||||||
|
animateProgressVisibility
|
||||||
|
? .easeInOut
|
||||||
|
: .easeInOut.delay(toastSlideAnimationDuration),
|
||||||
|
value: receiveModel.progress == nil
|
||||||
|
)
|
||||||
|
|
||||||
Button(action: onCancel) {
|
Button(action: onCancel) {
|
||||||
Image(systemName: "xmark")
|
Image(systemName: "xmark")
|
||||||
@@ -286,6 +295,7 @@ struct QuickDropToastHostView: View {
|
|||||||
let onCancel: () -> Void
|
let onCancel: () -> Void
|
||||||
|
|
||||||
@State private var didRenderOnce = false
|
@State private var didRenderOnce = false
|
||||||
|
@State private var hasFinishedSlideIn = false
|
||||||
|
|
||||||
private let slideAnimation = Animation.smooth
|
private let slideAnimation = Animation.smooth
|
||||||
private let fadeAnimation = Animation.easeInOut
|
private let fadeAnimation = Animation.easeInOut
|
||||||
@@ -298,7 +308,11 @@ struct QuickDropToastHostView: View {
|
|||||||
let hiddenBlur: CGFloat = isSlide ? 0 : 8
|
let hiddenBlur: CGFloat = isSlide ? 0 : 8
|
||||||
let animation = isSlide ? slideAnimation : fadeAnimation
|
let animation = isSlide ? slideAnimation : fadeAnimation
|
||||||
|
|
||||||
QuickDropToastView(receiveModel: receiveModel, onCancel: onCancel)
|
QuickDropToastView(
|
||||||
|
receiveModel: receiveModel,
|
||||||
|
animateProgressVisibility: hasFinishedSlideIn,
|
||||||
|
onCancel: onCancel
|
||||||
|
)
|
||||||
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
.frame(width: toastViewSize.width, height: toastViewSize.height)
|
||||||
.offset(x: isVisible ? 0 : hiddenOffset)
|
.offset(x: isVisible ? 0 : hiddenOffset)
|
||||||
.opacity(isVisible ? 1 : hiddenOpacity)
|
.opacity(isVisible ? 1 : hiddenOpacity)
|
||||||
@@ -314,7 +328,18 @@ struct QuickDropToastHostView: View {
|
|||||||
didRenderOnce = true
|
didRenderOnce = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task(id: isVisible) {
|
||||||
|
hasFinishedSlideIn = !isVisible || !isSlide
|
||||||
|
|
||||||
|
guard isVisible, isSlide else { return }
|
||||||
|
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(toastSlideAnimationDuration * 1_000_000_000))
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
hasFinishedSlideIn = true
|
||||||
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
hasFinishedSlideIn = false
|
||||||
didRenderOnce = false
|
didRenderOnce = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,8 +439,7 @@ struct AdaptiveToastBackgroundLayer: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
Color.clear
|
GlassEffectBackgroundView(cornerRadius: cornerRadius)
|
||||||
.glassEffect(.regular, in: .rect(cornerRadius: cornerRadius, style: .continuous))
|
|
||||||
} else {
|
} else {
|
||||||
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow, cornerRadius: cornerRadius)
|
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow, cornerRadius: cornerRadius)
|
||||||
}
|
}
|
||||||
@@ -424,6 +448,22 @@ struct AdaptiveToastBackgroundLayer: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
|
struct GlassEffectBackgroundView: NSViewRepresentable {
|
||||||
|
var cornerRadius: CGFloat = 0
|
||||||
|
|
||||||
|
func makeNSView(context: Context) -> NSGlassEffectView {
|
||||||
|
let v = NSGlassEffectView()
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ v: NSGlassEffectView, context: Context) {
|
||||||
|
v.cornerRadius = cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
|
// NSVisualEffectView wrapper for macOS 11 compatible blur/vibrancy
|
||||||
struct VisualEffectView: NSViewRepresentable {
|
struct VisualEffectView: NSViewRepresentable {
|
||||||
var material: NSVisualEffectView.Material = .hudWindow
|
var material: NSVisualEffectView.Material = .hudWindow
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ import SwiftUI
|
|||||||
struct TutorialView: View {
|
struct TutorialView: View {
|
||||||
|
|
||||||
let tab: Tab
|
let tab: Tab
|
||||||
let openPlus: () -> Void
|
let openAppAdvertisementView: () -> Void
|
||||||
|
|
||||||
@State private var licenseWindow: NSWindow?
|
|
||||||
@ObservedObject var iapManager = IAPManager.sharedInstance
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@@ -34,7 +31,7 @@ struct TutorialView: View {
|
|||||||
.padding(.top)
|
.padding(.top)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Text(tab.text.localized())
|
Text(tab.text)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.padding()
|
.padding()
|
||||||
@@ -43,55 +40,45 @@ struct TutorialView: View {
|
|||||||
.frame(width: 550)
|
.frame(width: 550)
|
||||||
|
|
||||||
if tab == .receive {
|
if tab == .receive {
|
||||||
HStack(spacing: 30) {
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
licenseWindow = openLicenseWindow()
|
showAndroidAirDropModeAlert()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Acknowledgements")
|
Text("AndroidAirDropModeHelpLink".localized())
|
||||||
.underline()
|
.underline()
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.opacity(0.5)
|
.multilineTextAlignment(.center)
|
||||||
|
.opacity(0.7)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
#if !GITHUB
|
|
||||||
if !iapManager.plusVersionState {
|
|
||||||
Button {
|
|
||||||
openPlus()
|
|
||||||
} label: {
|
|
||||||
Text("SupportQuickDrop")
|
|
||||||
.underline()
|
|
||||||
.font(.footnote)
|
|
||||||
.opacity(0.5)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Button {
|
|
||||||
IAPManager.sharedInstance.plusVersionState = false
|
|
||||||
Settings.sharedInstance.deleteAllUserDefaults()
|
|
||||||
} label: {
|
|
||||||
Text("ResetAllSettings")
|
|
||||||
.underline()
|
|
||||||
.font(.footnote)
|
|
||||||
.opacity(0.5)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
else if tab == .send {
|
else if tab == .send {
|
||||||
EnableExtensionView()
|
EnableExtensionView()
|
||||||
}
|
}
|
||||||
|
else if tab == .notificationSync || tab == .clipboardSync {
|
||||||
|
Button {
|
||||||
|
openAppAdvertisementView()
|
||||||
|
} label: {
|
||||||
|
Text("DownloadMobileApp".localized())
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showAndroidAirDropModeAlert() {
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
alert.messageText = "AndroidAirDropModeAlertTitle".localized()
|
||||||
|
alert.informativeText = "AndroidAirDropModeAlertMessage".localized()
|
||||||
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
|
ModelessAlertPresenter.present(alert)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct EnableExtensionView: View {
|
struct EnableExtensionView: View {
|
||||||
@State private var showSharePicker = false
|
@State private var showSharePicker = false
|
||||||
@@ -188,5 +175,5 @@ private struct SharingPickerPresenter: NSViewRepresentable {
|
|||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
|
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}, navigationState: WelcomeScreenNavigationState())
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,25 @@ import SwiftUI
|
|||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import LUI
|
import LUI
|
||||||
|
|
||||||
|
struct WelcomeScreenNavigationRequest {
|
||||||
|
let tab: Tab
|
||||||
|
let id = UUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
final class WelcomeScreenNavigationState: ObservableObject {
|
||||||
|
@Published private(set) var request: WelcomeScreenNavigationRequest
|
||||||
|
|
||||||
|
init(selection: Tab = .receive) {
|
||||||
|
self.request = WelcomeScreenNavigationRequest(tab: selection)
|
||||||
|
}
|
||||||
|
|
||||||
|
func select(_ tab: Tab) {
|
||||||
|
request = WelcomeScreenNavigationRequest(tab: tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct WelcomeScreen: View {
|
struct WelcomeScreen: View {
|
||||||
|
|
||||||
static let width: CGFloat = 1000
|
static let width: CGFloat = 1000
|
||||||
@@ -21,7 +40,23 @@ struct WelcomeScreen: View {
|
|||||||
let openCableTransmissionView: () -> Void
|
let openCableTransmissionView: () -> Void
|
||||||
let checkForNetworkIssues: () -> Void
|
let checkForNetworkIssues: () -> Void
|
||||||
|
|
||||||
@State private var selection: Tab = Tab.receive
|
@ObservedObject var navigationState: WelcomeScreenNavigationState
|
||||||
|
@State private var selection: Tab
|
||||||
|
|
||||||
|
init(
|
||||||
|
openPlusScreen: @escaping () -> Void,
|
||||||
|
openAppAdvertisementView: @escaping () -> Void,
|
||||||
|
openCableTransmissionView: @escaping () -> Void,
|
||||||
|
checkForNetworkIssues: @escaping () -> Void,
|
||||||
|
navigationState: WelcomeScreenNavigationState
|
||||||
|
) {
|
||||||
|
self.openPlusScreen = openPlusScreen
|
||||||
|
self.openAppAdvertisementView = openAppAdvertisementView
|
||||||
|
self.openCableTransmissionView = openCableTransmissionView
|
||||||
|
self.checkForNetworkIssues = checkForNetworkIssues
|
||||||
|
self.navigationState = navigationState
|
||||||
|
_selection = State(initialValue: navigationState.request.tab)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
||||||
@@ -35,7 +70,7 @@ struct WelcomeScreen: View {
|
|||||||
)
|
)
|
||||||
|
|
||||||
List(selection: listBinding) {
|
List(selection: listBinding) {
|
||||||
ForEach(Tab.allCases.filter({$0 != .settings }), id: \.self) { tab in
|
ForEach([Tab.receive, .send, .notificationSync, .clipboardSync], id: \.self) { tab in
|
||||||
Label(tab.sidebarTitle, systemImage: tab.systemImage)
|
Label(tab.sidebarTitle, systemImage: tab.systemImage)
|
||||||
.tag(tab)
|
.tag(tab)
|
||||||
.frame(height: 30)
|
.frame(height: 30)
|
||||||
@@ -43,8 +78,14 @@ struct WelcomeScreen: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
|
Label(Tab.troubleshooting.sidebarTitle, systemImage: Tab.troubleshooting.systemImage)
|
||||||
|
.tag(Tab.troubleshooting)
|
||||||
|
.frame(height: 30)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
|
ExternalLinkLabel(label: "GetSupport", icon: "questionmark.circle") {
|
||||||
SupportMail.sendSupportMail()
|
LogExportPresenter.showUploadLogsAlert(openSupportMailWhenNotUploading: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
|
ExternalLinkLabel(label: "PrivacyPolicy", icon: "hand.raised") {
|
||||||
@@ -76,10 +117,13 @@ struct WelcomeScreen: View {
|
|||||||
Color.defaultBackground.edgesIgnoringSafeArea(.vertical)
|
Color.defaultBackground.edgesIgnoringSafeArea(.vertical)
|
||||||
|
|
||||||
if selection == .settings {
|
if selection == .settings {
|
||||||
SettingsView()
|
SettingsView(openPlus: openPlusScreen)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
TutorialView(tab: selection, openPlus: openPlusScreen)
|
TutorialView(
|
||||||
|
tab: selection,
|
||||||
|
openAppAdvertisementView: openAppAdvertisementView
|
||||||
|
)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if selection == .troubleshooting {
|
if selection == .troubleshooting {
|
||||||
checkForNetworkIssues()
|
checkForNetworkIssues()
|
||||||
@@ -88,6 +132,11 @@ struct WelcomeScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(navigationState.$request) { request in
|
||||||
|
if selection != request.tab {
|
||||||
|
selection = request.tab
|
||||||
|
}
|
||||||
|
}
|
||||||
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||||
handleDrop(providers: providers)
|
handleDrop(providers: providers)
|
||||||
return true
|
return true
|
||||||
@@ -96,7 +145,7 @@ struct WelcomeScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openPrivacyPolicy() {
|
func openPrivacyPolicy() {
|
||||||
if let url = URL(string: "https://leonboettger.com/quickdrop-privacy") {
|
if let url = URL(string: .privacyPolicyURL) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,6 +226,8 @@ struct ExternalLinkLabel: View {
|
|||||||
enum Tab: CaseIterable {
|
enum Tab: CaseIterable {
|
||||||
case receive
|
case receive
|
||||||
case send
|
case send
|
||||||
|
case notificationSync
|
||||||
|
case clipboardSync
|
||||||
case troubleshooting
|
case troubleshooting
|
||||||
case settings
|
case settings
|
||||||
|
|
||||||
@@ -186,6 +237,10 @@ enum Tab: CaseIterable {
|
|||||||
return "ReceiveFiles".localized()
|
return "ReceiveFiles".localized()
|
||||||
case .send:
|
case .send:
|
||||||
return "SendFiles".localized()
|
return "SendFiles".localized()
|
||||||
|
case .notificationSync:
|
||||||
|
return "NotificationSyncSidebar".localized()
|
||||||
|
case .clipboardSync:
|
||||||
|
return "ClipboardSyncSidebar".localized()
|
||||||
case .troubleshooting:
|
case .troubleshooting:
|
||||||
return "TroubleshootingAndFaq".localized()
|
return "TroubleshootingAndFaq".localized()
|
||||||
case .settings:
|
case .settings:
|
||||||
@@ -203,9 +258,13 @@ enum Tab: CaseIterable {
|
|||||||
var text: String {
|
var text: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .receive:
|
case .receive:
|
||||||
"UserManualDescription"
|
"UserManualDescription".localized()
|
||||||
case .send:
|
case .send:
|
||||||
"SendFilesDescription"
|
"SendFilesDescription".localized()
|
||||||
|
case .notificationSync:
|
||||||
|
"NotificationSyncManualDescription".localized()
|
||||||
|
case .clipboardSync:
|
||||||
|
"ClipboardSyncManualDescription".localized()
|
||||||
default:
|
default:
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -217,6 +276,10 @@ enum Tab: CaseIterable {
|
|||||||
return "tray.and.arrow.down"
|
return "tray.and.arrow.down"
|
||||||
case .send:
|
case .send:
|
||||||
return "tray.and.arrow.up"
|
return "tray.and.arrow.up"
|
||||||
|
case .notificationSync:
|
||||||
|
return "bell"
|
||||||
|
case .clipboardSync:
|
||||||
|
return "clipboard"
|
||||||
case .troubleshooting:
|
case .troubleshooting:
|
||||||
return "exclamationmark.triangle"
|
return "exclamationmark.triangle"
|
||||||
case .settings:
|
case .settings:
|
||||||
@@ -227,5 +290,5 @@ enum Tab: CaseIterable {
|
|||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {})
|
WelcomeScreen(openPlusScreen: {}, openAppAdvertisementView: {}, openCableTransmissionView: {}, checkForNetworkIssues: {}, navigationState: WelcomeScreenNavigationState())
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if transfer.type == .notificationSync {
|
if transfer.type == .notificationSync || transfer.type == .clipboardSync {
|
||||||
self.hideQuickDropToast()
|
self.hideQuickDropToast()
|
||||||
self.toastActions = nil
|
self.toastActions = nil
|
||||||
self.progress = nil
|
self.progress = nil
|
||||||
@@ -93,7 +93,10 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized()
|
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized()
|
||||||
self.activeDeviceName = resolvedSenderName
|
self.activeDeviceName = resolvedSenderName
|
||||||
|
|
||||||
let compareMessage = "NotificationSyncToastQrSubtitle".localized()
|
let pairingUseCase: PairingUseCase = transfer.type == .clipboardSync ? .clipboardSync : .notificationSync
|
||||||
|
let compareMessage = transfer.type == .clipboardSync
|
||||||
|
? "ClipboardSyncToastQrSubtitle".localized()
|
||||||
|
: "NotificationSyncToastQrSubtitle".localized()
|
||||||
self.consentState = ConsentToastState(
|
self.consentState = ConsentToastState(
|
||||||
transferID: transferID,
|
transferID: transferID,
|
||||||
pinCodeMessage: "",
|
pinCodeMessage: "",
|
||||||
@@ -105,17 +108,18 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
withAnimation {
|
withAnimation {
|
||||||
self.consentState = nil
|
self.consentState = nil
|
||||||
}
|
}
|
||||||
guard let receiverFingerprint = NotificationSyncPairingToken.receiverFingerprintHex() else {
|
guard let receiverFingerprint = PairingToken.receiverFingerprintHex() else {
|
||||||
log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.")
|
log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.")
|
||||||
secondaryButtonAction()
|
secondaryButtonAction()
|
||||||
self.hideQuickDropToast(style: .fade)
|
self.hideQuickDropToast(style: .fade)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let pairingToken = NotificationSyncPairingToken.generate()
|
let pairingToken = PairingToken.generate()
|
||||||
NotificationSyncQrSheetManager.shared.open(
|
PairingQrSheetManager.shared.open(
|
||||||
token: pairingToken,
|
token: pairingToken,
|
||||||
receiverFingerprint: receiverFingerprint,
|
receiverFingerprint: receiverFingerprint,
|
||||||
deviceName: resolvedSenderName
|
deviceName: resolvedSenderName,
|
||||||
|
useCase: pairingUseCase
|
||||||
)
|
)
|
||||||
NearbyConnectionManager.shared.submitUserConsent(
|
NearbyConnectionManager.shared.submitUserConsent(
|
||||||
transferID: transferID,
|
transferID: transferID,
|
||||||
@@ -185,6 +189,11 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
|
|
||||||
|
|
||||||
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo) {
|
||||||
|
#if os(macOS)
|
||||||
|
if transfer.isClipboardSync {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
|
let mainMessage = transfer.getDescription(deviceName: device.name ?? "AndroidDevice".localized())
|
||||||
|
|
||||||
@@ -232,19 +241,19 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func notificationSyncSetupConfirmed() {
|
func pairingSetupConfirmed() {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.consentState?.notificationSyncStage != nil {
|
if self.consentState?.notificationSyncStage != nil {
|
||||||
self.hideQuickDropToast(style: .fade)
|
self.hideQuickDropToast(style: .fade)
|
||||||
}
|
}
|
||||||
NotificationSyncQrSheetManager.shared.close()
|
PairingQrSheetManager.shared.close()
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, error: (any Error)?) {
|
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: (any Error)?) {
|
||||||
|
|
||||||
processes.removeValue(forKey: connectionID)
|
processes.removeValue(forKey: connectionID)
|
||||||
|
|
||||||
@@ -282,7 +291,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
controlPlusScreen(false)
|
controlPlusScreen(false)
|
||||||
|
|
||||||
errorVibration()
|
errorVibration()
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error)
|
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: device?.isQuickDropPeer ?? false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
@@ -350,7 +359,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
alert.informativeText = error.localizedDescription
|
alert.informativeText = error.localizedDescription
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
alert.runModal()
|
ModelessAlertPresenter.present(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,6 +397,9 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
if wasPendingConsent {
|
if wasPendingConsent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if wasClipboardSync && error == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if let error = error {
|
if let error = error {
|
||||||
if let name = device?.name {
|
if let name = device?.name {
|
||||||
@@ -396,7 +408,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
controlPlusScreen(false)
|
controlPlusScreen(false)
|
||||||
|
|
||||||
errorVibration()
|
errorVibration()
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error)
|
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: device?.isQuickDropPeer ?? false)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -441,7 +453,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
|
|||||||
alert.informativeText = error.localizedDescription
|
alert.informativeText = error.localizedDescription
|
||||||
alert.addButton(withTitle: "CloseAlert".localized())
|
alert.addButton(withTitle: "CloseAlert".localized())
|
||||||
|
|
||||||
alert.runModal()
|
ModelessAlertPresenter.present(alert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
|
|
||||||
if let name = selectedDevice?.name {
|
if let name = selectedDevice?.name {
|
||||||
errorVibration()
|
errorVibration()
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error)
|
ErrorAlertHandler.shared.showErrorAlert(for: name, error: error, isQuickDropPeer: selectedDevice?.isQuickDropPeer ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
progressValue = nil
|
progressValue = nil
|
||||||
@@ -129,7 +129,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
selectedDevice = device
|
selectedDevice = device
|
||||||
|
|
||||||
runAfter(seconds: 0.3) {
|
runAfter(seconds: 0.3) {
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
|
delegate: self,
|
||||||
|
urls: self.urls,
|
||||||
|
textToSend: self.textToSend,
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
|
)
|
||||||
self.progressState = "Connecting".localized()
|
self.progressState = "Connecting".localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreBluetooth
|
||||||
|
import Testing
|
||||||
|
@testable import QuickDrop
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
struct AndroidAirDropModeDetectorTests {
|
||||||
|
@Test
|
||||||
|
func detectsWhenAirDropModeSignalsPassThreshold() {
|
||||||
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
|
||||||
|
|
||||||
|
let firstAirDropMode = classifier.record(.airDropMode(rssi: -58))
|
||||||
|
#expect(!firstAirDropMode.isDetected)
|
||||||
|
|
||||||
|
let detected = classifier.record(.airDropMode(rssi: -56))
|
||||||
|
#expect(detected.isDetected)
|
||||||
|
#expect(detected.airDropModePacketCount == 2)
|
||||||
|
#expect(detected.strongestAirDropModeRSSI == -56)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func matchesObservedMoseyAirDropSuffixVariants() {
|
||||||
|
#expect(
|
||||||
|
AndroidAirDropModeClassifier.isAirDropModeAdvertisement(
|
||||||
|
.airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: -58)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#expect(
|
||||||
|
AndroidAirDropModeClassifier.isAirDropModeAdvertisement(
|
||||||
|
.airDropMode(manufacturerDataSuffix: "03b429412f624af300", rssi: -58)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func ignoresSignalsBelowRSSIThreshold() {
|
||||||
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
|
||||||
|
|
||||||
|
classifier.record(.airDropMode(rssi: -72))
|
||||||
|
let observation = classifier.record(.airDropMode(rssi: -85))
|
||||||
|
|
||||||
|
#expect(!observation.isDetected)
|
||||||
|
#expect(observation.airDropModePacketCount == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@Test
|
||||||
|
func iOSDetectsNonConnectableFCF1AdvertisementsWithoutServiceData() {
|
||||||
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 2)
|
||||||
|
|
||||||
|
let firstAirDropMode = classifier.record(.iOSAirDropMode(rssi: -62))
|
||||||
|
#expect(!firstAirDropMode.isDetected)
|
||||||
|
|
||||||
|
let detected = classifier.record(.iOSAirDropMode(rssi: -61))
|
||||||
|
#expect(detected.isDetected)
|
||||||
|
#expect(detected.airDropModePacketCount == 2)
|
||||||
|
#expect(detected.strongestAirDropModeRSSI == -61)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func iOSIgnoresBackgroundFCF1AdvertisementsWithServiceData() {
|
||||||
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
|
||||||
|
|
||||||
|
classifier.record(.iOSBackgroundFCF1(rssi: -55))
|
||||||
|
let observation = classifier.record(.iOSBackgroundFCF1(rssi: -56))
|
||||||
|
|
||||||
|
#expect(!observation.isDetected)
|
||||||
|
#expect(observation.airDropModePacketCount == 0)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
@Test
|
||||||
|
func ignoresNearbyAppleAndBackgroundFCF1Advertisements() {
|
||||||
|
var classifier = AndroidAirDropModeClassifier(minimumRSSI: -70, requiredPacketsPerSignal: 1)
|
||||||
|
|
||||||
|
classifier.record(.advertisement(rssi: -50, manufacturerDataHex: "4c001006031dc4e5be78", serviceUUIDs: []))
|
||||||
|
classifier.record(.advertisement(rssi: -55, manufacturerDataHex: nil, serviceUUIDs: ["FCF1"]))
|
||||||
|
classifier.record(.advertisement(rssi: -56, manufacturerDataHex: "4c00051200000000000000000128b429412f624af300", serviceUUIDs: []))
|
||||||
|
let observation = classifier.record(
|
||||||
|
.advertisement(
|
||||||
|
rssi: -56,
|
||||||
|
manufacturerDataHex: "4c00051200000000000000000028b429412f624af300",
|
||||||
|
serviceUUIDs: ["FCF1"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#expect(!observation.isDetected)
|
||||||
|
#expect(observation.airDropModePacketCount == 0)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func scansOnlyForAirDropModeServiceAndAllowsDuplicatePackets() {
|
||||||
|
#expect(AndroidAirDropModeDetector.scanServiceUUIDs.map(\.uuidString) == [AndroidAirDropModeClassifier.airDropModeServiceUUID])
|
||||||
|
#expect(AndroidAirDropModeDetector.scanOptions[CBCentralManagerScanOptionAllowDuplicatesKey] as? Bool == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AndroidAirDropAdvertisement {
|
||||||
|
static func iOSAirDropMode(rssi: Int) -> Self {
|
||||||
|
advertisement(
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerDataHex: nil,
|
||||||
|
serviceUUIDs: ["FCF1"],
|
||||||
|
isConnectable: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func iOSBackgroundFCF1(rssi: Int) -> Self {
|
||||||
|
advertisement(
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerDataHex: nil,
|
||||||
|
serviceUUIDs: ["FCF1"],
|
||||||
|
serviceDataHexByUUID: ["FCF1": "0407329497cd97ce4739a79920b877a45bb8d6d1a7"],
|
||||||
|
isConnectable: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func airDropMode(rssi: Int) -> Self {
|
||||||
|
airDropMode(manufacturerDataSuffix: "28b429412f624af300", rssi: rssi)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func airDropMode(manufacturerDataSuffix: String, rssi: Int) -> Self {
|
||||||
|
advertisement(
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerDataHex: "4c000512000000000000000001\(manufacturerDataSuffix)",
|
||||||
|
serviceUUIDs: ["FCF1"]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func advertisement(
|
||||||
|
rssi: Int,
|
||||||
|
manufacturerDataHex: String?,
|
||||||
|
serviceUUIDs: Set<String>,
|
||||||
|
serviceDataHexByUUID: [String: String] = [:],
|
||||||
|
isConnectable: Bool? = nil
|
||||||
|
) -> Self {
|
||||||
|
AndroidAirDropAdvertisement(
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerDataHex: manufacturerDataHex,
|
||||||
|
serviceUUIDs: serviceUUIDs,
|
||||||
|
serviceDataHexByUUID: serviceDataHexByUUID,
|
||||||
|
isConnectable: isConnectable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,472 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"NSBluetoothAlwaysUsageDescription" : {
|
||||||
|
"comment" : "Privacy - Bluetooth Always Usage Description",
|
||||||
|
"localizations" : {
|
||||||
|
"ar" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ca" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cs" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"da" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"el" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-AU" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-419" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"he" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nb" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-BR" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-PT" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ro" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sv" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"th" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uk" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"NSBluetoothPeripheralUsageDescription" : {
|
||||||
|
"comment" : "Privacy - Bluetooth Peripheral Usage Description",
|
||||||
|
"localizations" : {
|
||||||
|
"ar" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "يستخدم QuickDrop تقنية Bluetooth لتحسين اكتشاف الأجهزة القريبة."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ca" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilitza Bluetooth per millorar la detecció de dispositius propers."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cs" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop používá Bluetooth pro vylepšenou detekci zařízení v okolí."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"da" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruger Bluetooth til forbedret registrering af enheder i nærheden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop verwendet Bluetooth für eine verbesserte Erkennung von Geräten in der Nähe."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"el" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Το QuickDrop χρησιμοποιεί Bluetooth για βελτιωμένο εντοπισμό κοντινών συσκευών."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-AU" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop uses Bluetooth for enhanced nearby device detection."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"es-419" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa Bluetooth para mejorar la detección de dispositivos cercanos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop käyttää Bluetoothia läheisten laitteiden parempaan havaitsemiseen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fr-CA" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop utilise le Bluetooth pour améliorer la détection des appareils à proximité."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"he" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop משתמש ב-Bluetooth לזיהוי משופר של מכשירים בקרבת מקום."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop आस-पास के डिवाइस की बेहतर पहचान के लिए Bluetooth का उपयोग करता है।"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop koristi Bluetooth za poboljšano otkrivanje uređaja u blizini."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "A QuickDrop Bluetoothot használ a közeli eszközök fejlettebb felismeréséhez."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk deteksi perangkat terdekat yang lebih baik."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"it" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop usa il Bluetooth per un rilevamento migliorato dei dispositivi nelle vicinanze."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDropは、近くのデバイスをより正確に検出するためにBluetoothを使用します。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ko" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop은 주변 기기를 더 잘 감지하기 위해 Bluetooth를 사용합니다."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop menggunakan Bluetooth untuk pengesanan peranti berdekatan yang dipertingkatkan."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nb" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop bruker Bluetooth for forbedret oppdagelse av enheter i nærheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop gebruikt Bluetooth voor verbeterde detectie van apparaten in de buurt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pl" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop używa Bluetooth do ulepszonego wykrywania pobliskich urządzeń."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-BR" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a detecção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pt-PT" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "O QuickDrop usa Bluetooth para melhorar a deteção de dispositivos próximos."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ro" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop folosește Bluetooth pentru detectarea îmbunătățită a dispozitivelor din apropiere."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ru" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop использует Bluetooth для улучшенного обнаружения устройств поблизости."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sv" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop använder Bluetooth för förbättrad upptäckt av enheter i närheten."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"th" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop ใช้ Bluetooth เพื่อการตรวจจับอุปกรณ์ใกล้เคียงที่ดียิ่งขึ้น"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop, yakındaki cihazları daha iyi algılamak için Bluetooth kullanır."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uk" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop використовує Bluetooth для покращеного виявлення пристроїв поблизу."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vi" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop sử dụng Bluetooth để phát hiện thiết bị lân cận tốt hơn."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用蓝牙来增强附近设备检测。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"zh-Hant" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "QuickDrop 使用藍牙來增強附近裝置偵測。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"NSHumanReadableCopyright" : {
|
"NSHumanReadableCopyright" : {
|
||||||
"comment" : "Copyright (human-readable)",
|
"comment" : "Copyright (human-readable)",
|
||||||
"extractionState" : "extracted_with_value",
|
"extractionState" : "extracted_with_value",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
|
|
||||||
func connectionFailed(with error: any Error) {
|
func connectionFailed(with error: any Error) {
|
||||||
|
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error)
|
ErrorAlertHandler.shared.showErrorAlert(for: selectedDevice?.name ?? "?", error: error, isQuickDropPeer: selectedDevice?.isQuickDropPeer ?? false)
|
||||||
|
|
||||||
progressValue = nil
|
progressValue = nil
|
||||||
selectedDevice = nil
|
selectedDevice = nil
|
||||||
@@ -84,7 +84,13 @@ class SendModel: ObservableObject, OutboundAppDelegate {
|
|||||||
progressValue = 0
|
progressValue = 0
|
||||||
progressState = "Connecting".localized()
|
progressState = "Connecting".localized()
|
||||||
selectedDevice = device
|
selectedDevice = device
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: urls, textToSend: textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
|
delegate: self,
|
||||||
|
urls: urls,
|
||||||
|
textToSend: textToSend,
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,51 @@ struct ContentView: View {
|
|||||||
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
@ObservedObject var luiSettings = LUISettings.sharedInstance
|
||||||
@ObservedObject var iapManager = IAPManager.sharedInstance
|
@ObservedObject var iapManager = IAPManager.sharedInstance
|
||||||
|
|
||||||
|
#if !EXTENSION
|
||||||
|
@ObservedObject private var bluetoothPermission = BluetoothPermissionManager.shared
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
@State private var showAndroidAirDropModeIssue = false
|
||||||
|
@State private var didShowAndroidAirDropModeIssue = false
|
||||||
|
@State private var shouldRequestBluetoothPermissionAutomatically = LUISettings.sharedInstance.appLaunchedBefore
|
||||||
|
#endif
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
#if !EXTENSION
|
||||||
|
rootContent
|
||||||
|
.sheet(isPresented: $showAndroidAirDropModeIssue) {
|
||||||
|
NavigationView {
|
||||||
|
AndroidAirDropModeIssueView()
|
||||||
|
.navigationTitle(String("QuickDrop"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarItems(trailing: XButton(action: { showAndroidAirDropModeIssue = false }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
requestBluetoothPermissionForExistingUserIfNeeded()
|
||||||
|
startAndroidAirDropModeDetectionIfAllowed()
|
||||||
|
}
|
||||||
|
.onChange(of: bluetoothPermission.authorization) { _ in
|
||||||
|
startAndroidAirDropModeDetectionIfAllowed()
|
||||||
|
}
|
||||||
|
.onChange(of: scenePhase) { newValue in
|
||||||
|
switch newValue {
|
||||||
|
case .active:
|
||||||
|
requestBluetoothPermissionForExistingUserIfNeeded()
|
||||||
|
startAndroidAirDropModeDetectionIfAllowed()
|
||||||
|
case .background:
|
||||||
|
AndroidAirDropModeDetector.shared.stop()
|
||||||
|
case .inactive:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
rootContent
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rootContent: some View {
|
||||||
AppRootView(phoneView: {
|
AppRootView(phoneView: {
|
||||||
DeviceListView()
|
DeviceListView()
|
||||||
.environment(\.sheetActive, isShareExtension())
|
.environment(\.sheetActive, isShareExtension())
|
||||||
@@ -26,16 +70,81 @@ struct ContentView: View {
|
|||||||
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
NavigationLinkLabel(imageName: "folder.fill", text: "BrowseDownloadedFiles")
|
||||||
}
|
}
|
||||||
|
|
||||||
LUILink(destination: NavigationSubView(header: "TrustedDevices") {
|
LUILink(destination: TrustedDevicesView()) {
|
||||||
TrustedDevicesView()
|
|
||||||
}) {
|
|
||||||
NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green)
|
NavigationLinkLabel(imageName: "checkmark.shield.fill", text: "ManageTrustedDevices", backgroundColor: .green)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomSection {
|
||||||
|
LUILink(destination: QuickDropFAQSettingsView()) {
|
||||||
|
NavigationLinkLabel(imageName: "questionmark.circle.fill", text: "TroubleshootingAndFaq", backgroundColor: .orange)
|
||||||
|
}
|
||||||
|
}
|
||||||
}, firstIntroductionView: {
|
}, firstIntroductionView: {
|
||||||
LocalNetworkPermissionView()
|
LocalNetworkPermissionView()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !EXTENSION
|
||||||
|
private func requestBluetoothPermissionForExistingUserIfNeeded() {
|
||||||
|
guard shouldRequestBluetoothPermissionAutomatically else { return }
|
||||||
|
|
||||||
|
requestBluetoothPermissionIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestBluetoothPermissionIfNeeded() {
|
||||||
|
bluetoothPermission.refresh()
|
||||||
|
|
||||||
|
guard bluetoothPermission.canRequestPermission else { return }
|
||||||
|
|
||||||
|
log("[ContentView] Requesting Bluetooth permission for Android AirDrop mode detection.")
|
||||||
|
|
||||||
|
bluetoothPermission.requestPermission { allowed in
|
||||||
|
log("[ContentView] Bluetooth permission request completed. allowed=\(allowed)")
|
||||||
|
|
||||||
|
guard allowed else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
startAndroidAirDropModeDetectionIfAllowed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startAndroidAirDropModeDetectionIfAllowed() {
|
||||||
|
guard !didShowAndroidAirDropModeIssue else { return }
|
||||||
|
|
||||||
|
bluetoothPermission.refresh()
|
||||||
|
|
||||||
|
guard bluetoothPermission.isAllowed else { return }
|
||||||
|
|
||||||
|
AndroidAirDropModeDetector.shared.start { _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard !didShowAndroidAirDropModeIssue else { return }
|
||||||
|
|
||||||
|
didShowAndroidAirDropModeIssue = true
|
||||||
|
showAndroidAirDropModeIssue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct QuickDropFAQSettingsView: View {
|
||||||
|
|
||||||
|
private let faqItems: [FAQItem] = [
|
||||||
|
.init(question: "FaqNotVisibleOrConnectingQuestion", answer: "FaqNotVisibleOrConnectingAnswer"),
|
||||||
|
.init(question: "FaqPhotoDateNotPreservedQuestion", answer: "FaqPhotoDateNotPreservedAnswer"),
|
||||||
|
.init(question: "FaqAndroidDeviceNotVisibleQuestion", answer: "FaqAndroidDeviceNotVisibleAnswer"),
|
||||||
|
.init(question: "FaqTrustedDevicesQuestion", answer: "FaqTrustedDevicesAnswer"),
|
||||||
|
.init(question: "MultipleFilesSendingQuestion", answer: "MultipleFilesSendingAnswer"),
|
||||||
|
.init(question: "FaqBugQuestion", answer: "FaqBugAnswer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationSubView(header: "TroubleshootingAndFaq") {
|
||||||
|
FAQView(faqItems: faqItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceListView: View {
|
struct DeviceListView: View {
|
||||||
@@ -65,7 +174,7 @@ struct DeviceListView: View {
|
|||||||
|
|
||||||
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
let showsLoadingIndicator = nearbyConnectionManager.hasLocalNetworkPermission && nearbyConnectionManager.isConnectedToLocalNetwork
|
||||||
|
|
||||||
BottomBarView(header: "QuickDrop", navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
BottomBarView(header: "QuickDrop", ignoresKeyboard: true, navigationBarLayout: isShareExtension() ? .SmallOnlyAlways : .Default, bottomViewHeight: 30) {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
let attachments = nearbyConnectionManager.attachments
|
let attachments = nearbyConnectionManager.attachments
|
||||||
@@ -288,14 +397,25 @@ struct DeviceButtonLabel: View {
|
|||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
HStack(spacing: 5) {
|
||||||
Text(name)
|
Text(name)
|
||||||
.fontWeight(.medium)
|
.foregroundColor(.mainColor)
|
||||||
|
|
||||||
|
if device.isQuickDropPeer {
|
||||||
|
Image(.menuBarIcon)
|
||||||
|
.renderingMode(.template)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: 14, height: 14)
|
||||||
.foregroundColor(Color.primary)
|
.foregroundColor(Color.primary)
|
||||||
|
.opacity(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(isSelected ? progress ?? "..." : "Available".localized())
|
Text(isSelected ? progress ?? "..." : "Available".localized())
|
||||||
.font(.system(size: 12))
|
.foregroundColor(.mainColor.opacity(0.6))
|
||||||
.foregroundColor(Color.primary.opacity(0.6))
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ struct SendPickerButton<Label: View>: View {
|
|||||||
selectedPhotoItems = []
|
selectedPhotoItems = []
|
||||||
} catch {
|
} catch {
|
||||||
log("Could not fetch photo URLs: \(error)")
|
log("Could not fetch photo URLs: \(error)")
|
||||||
|
onResult(nil, nil) // let the caller dismiss the "preparing" it opened on `onPrepare`
|
||||||
|
selectedPhotoItems = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ public struct LocalNetworkPermissionView: View {
|
|||||||
let manager = NearbyConnectionManager.shared
|
let manager = NearbyConnectionManager.shared
|
||||||
manager.startDeviceDiscovery()
|
manager.startDeviceDiscovery()
|
||||||
}
|
}
|
||||||
}, continueLabel: "introduction_continue", canSkip: false, nextView: {
|
}, continueLabel: "introduction_continue", nextView: {
|
||||||
|
#if !EXTENSION
|
||||||
|
BluetoothPermissionView()
|
||||||
|
#else
|
||||||
IntroductionDoneView()
|
IntroductionDoneView()
|
||||||
}, nextViewSkip: {}, topView: {
|
#endif
|
||||||
|
}, topView: {
|
||||||
IntroductionIconView(icon: "wifi")
|
IntroductionIconView(icon: "wifi")
|
||||||
})
|
})
|
||||||
.onChange(of: scenePhase) { newValue in
|
.onChange(of: scenePhase) { newValue in
|
||||||
@@ -41,6 +45,49 @@ public struct LocalNetworkPermissionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !EXTENSION
|
||||||
|
private struct BluetoothPermissionView: View {
|
||||||
|
|
||||||
|
@ObservedObject private var bluetoothPermission = BluetoothPermissionManager.shared
|
||||||
|
@State private var requestedBluetoothAccess = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
PermissionView(title: "BluetoothPermissionRequiredTitle",
|
||||||
|
watchSymbol: "",
|
||||||
|
subtitle: "BluetoothPermissionRequiredSubtitle",
|
||||||
|
permissionAction: requestBluetoothAccess,
|
||||||
|
continueLabel: "introduction_continue",
|
||||||
|
nextView: {
|
||||||
|
IntroductionDoneView()
|
||||||
|
},
|
||||||
|
topView: {
|
||||||
|
IntroductionIconView(icon: "antenna.radiowaves.left.and.right")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestBluetoothAccess() {
|
||||||
|
requestedBluetoothAccess = true
|
||||||
|
bluetoothPermission.refresh()
|
||||||
|
|
||||||
|
if bluetoothPermission.isAllowed {
|
||||||
|
IntroductionViewController.sharedInstance.canProceed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard bluetoothPermission.canRequestPermission else {
|
||||||
|
IntroductionViewController.sharedInstance.canProceed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bluetoothPermission.requestPermission { _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
IntroductionViewController.sharedInstance.canProceed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
|
|||||||
@@ -92,9 +92,15 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
flowLayout.minimumLineSpacing = 10
|
flowLayout.minimumLineSpacing = 10
|
||||||
listView!.collectionViewLayout = flowLayout
|
listView!.collectionViewLayout = flowLayout
|
||||||
listView!.dataSource = self
|
listView!.dataSource = self
|
||||||
|
listView!.delegate = self
|
||||||
|
listView!.isSelectable = true
|
||||||
|
listView!.allowsMultipleSelection = false
|
||||||
|
|
||||||
progressDeviceIconWrap!.wantsLayer = true
|
progressDeviceIconWrap!.wantsLayer = true
|
||||||
progressDeviceIconWrap!.layer!.masksToBounds = false
|
progressDeviceIconWrap!.layer!.masksToBounds = false
|
||||||
|
progressDeviceSecondaryIcon?.image = NSImage(named: "MenuBarIcon")
|
||||||
|
progressDeviceSecondaryIcon?.image?.isTemplate = true
|
||||||
|
progressDeviceSecondaryIcon?.contentTintColor = .white
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -112,9 +118,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
timeoutDispatchWorkItem?.cancel()
|
timeoutDispatchWorkItem?.cancel()
|
||||||
|
|
||||||
if chosenDevice == nil {
|
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
}
|
|
||||||
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
NearbyConnectionManager.shared.removeOutboundAppDelegate(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +228,13 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
progressProgressBar?.doubleValue = 0
|
progressProgressBar?.doubleValue = 0
|
||||||
lastError = error
|
lastError = error
|
||||||
|
|
||||||
ErrorAlertHandler.shared.showErrorAlert(for: chosenDevice?.name ?? "", error: error)
|
ErrorAlertHandler.shared.showErrorAlert(
|
||||||
self.extensionContext!.cancelRequest(withError: error)
|
for: chosenDevice?.name ?? "",
|
||||||
|
error: error,
|
||||||
|
isQuickDropPeer: chosenDevice?.isQuickDropPeer ?? false
|
||||||
|
) { [weak self] in
|
||||||
|
self?.extensionContext?.cancelRequest(withError: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -246,20 +255,25 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
|
|
||||||
func selectDevice(device: RemoteDeviceInfo) {
|
func selectDevice(device: RemoteDeviceInfo) {
|
||||||
|
guard chosenDevice == nil else { return }
|
||||||
|
|
||||||
listViewWrapper?.animator().isHidden = true
|
listViewWrapper?.animator().isHidden = true
|
||||||
dontSeeDeviceButton?.animator().isHidden = true
|
dontSeeDeviceButton?.animator().isHidden = true
|
||||||
progressView?.animator().isHidden = false
|
progressView?.animator().isHidden = false
|
||||||
progressDeviceName?.stringValue = getDeviceName(device: device)
|
progressDeviceName?.stringValue = getDeviceName(device: device)
|
||||||
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
|
progressDeviceIcon?.image = imageForDeviceType(type: device.type)
|
||||||
|
progressDeviceSecondaryIcon?.isHidden = !device.isQuickDropPeer
|
||||||
progressProgressBar?.startAnimation(nil)
|
progressProgressBar?.startAnimation(nil)
|
||||||
progressState?.stringValue = "Preparing".localized()
|
progressState?.stringValue = "Preparing".localized()
|
||||||
chosenDevice = device
|
chosenDevice = device
|
||||||
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
|
||||||
|
|
||||||
runAfter(seconds: 0.3) {
|
runAfter(seconds: 0.3) {
|
||||||
NearbyConnectionManager.shared.startOutgoingTransfer(deviceID: device.id!, delegate: self, urls: self.urls, textToSend: self.textToSend)
|
NearbyConnectionManager.shared.startOutgoingTransfer(
|
||||||
|
deviceID: device.id!,
|
||||||
|
delegate: self,
|
||||||
|
urls: self.urls,
|
||||||
|
textToSend: self.textToSend,
|
||||||
|
preparationFinished: {
|
||||||
self.progressState?.stringValue = "Connecting".localized()
|
self.progressState?.stringValue = "Connecting".localized()
|
||||||
|
|
||||||
let timeoutAlert = DispatchWorkItem {
|
let timeoutAlert = DispatchWorkItem {
|
||||||
@@ -289,6 +303,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate {
|
|||||||
|
|
||||||
self.timeoutDispatchWorkItem = timeoutAlert
|
self.timeoutDispatchWorkItem = timeoutAlert
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
|
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0, execute: timeoutAlert)
|
||||||
|
},
|
||||||
|
receiverAuthenticationPolicy: .none
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,10 +401,13 @@ extension ShareViewController: NSCollectionViewDataSource {
|
|||||||
|
|
||||||
guard let collectionViewItem = item as? DeviceListCell else { return item }
|
guard let collectionViewItem = item as? DeviceListCell else { return item }
|
||||||
|
|
||||||
let device = foundDevices[indexPath[1]]
|
let device = foundDevices[indexPath.item]
|
||||||
|
|
||||||
collectionViewItem.textField?.stringValue = getDeviceName(device: device)
|
collectionViewItem.configure(
|
||||||
collectionViewItem.imageView?.image = imageForDeviceType(type: device.type)
|
deviceName: getDeviceName(device: device),
|
||||||
|
deviceImage: imageForDeviceType(type: device.type),
|
||||||
|
showsQuickDropPeerMarker: device.isQuickDropPeer && device.type == .phone
|
||||||
|
)
|
||||||
collectionViewItem.clickHandler = {
|
collectionViewItem.clickHandler = {
|
||||||
self.selectDevice(device: device)
|
self.selectDevice(device: device)
|
||||||
}
|
}
|
||||||
@@ -406,6 +426,20 @@ extension ShareViewController: NSCollectionViewDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension ShareViewController: NSCollectionViewDelegate {
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
|
||||||
|
guard let indexPath = indexPaths.first else { return }
|
||||||
|
|
||||||
|
let itemIndex = indexPath.item
|
||||||
|
guard foundDevices.indices.contains(itemIndex) else { return }
|
||||||
|
|
||||||
|
collectionView.deselectItems(at: indexPaths)
|
||||||
|
selectDevice(device: foundDevices[itemIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fileprivate func openPrivacyAndSecuritySettings() {
|
fileprivate func openPrivacyAndSecuritySettings() {
|
||||||
guard let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension") else { return }
|
guard let url = URL(string: "x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension") else { return }
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
|
|||||||
@@ -177,17 +177,17 @@ Gw
|
|||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="rb3-78-PXe"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
|
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="BEO-kU-a2Q">
|
||||||
<rect key="frame" x="24" y="-8" width="32" height="32"/>
|
<rect key="frame" x="16.800000000000001" y="16.800000000000001" width="14.4" height="14.4"/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="height" constant="32" id="05I-Ef-84Z"/>
|
<constraint firstAttribute="height" constant="14.4" id="05I-Ef-84Z"/>
|
||||||
<constraint firstAttribute="width" constant="32" id="V0g-Fz-HWH"/>
|
<constraint firstAttribute="width" constant="14.4" id="V0g-Fz-HWH"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="NSCaution" id="k1c-Xl-fTQ"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="k1c-Xl-fTQ"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailing" secondItem="BEO-kU-a2Q" secondAttribute="trailing" constant="-8" id="59U-Zi-ESa"/>
|
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerX" secondItem="84U-lB-SRB" secondAttribute="centerX" id="59U-Zi-ESa"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="BEO-kU-a2Q" secondAttribute="bottom" constant="-8" id="8ty-pq-miR"/>
|
<constraint firstItem="BEO-kU-a2Q" firstAttribute="centerY" secondItem="84U-lB-SRB" secondAttribute="centerY" id="8ty-pq-miR"/>
|
||||||
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
|
<constraint firstItem="84U-lB-SRB" firstAttribute="top" secondItem="jBg-Ih-Gac" secondAttribute="top" id="MB9-UI-2mb"/>
|
||||||
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
|
<constraint firstItem="84U-lB-SRB" firstAttribute="leading" secondItem="jBg-Ih-Gac" secondAttribute="leading" id="b23-hJ-aWT"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
|
<constraint firstAttribute="trailing" secondItem="84U-lB-SRB" secondAttribute="trailing" id="eKl-3V-K49"/>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Cocoa
|
|||||||
|
|
||||||
class DeviceListCell: NSCollectionViewItem {
|
class DeviceListCell: NSCollectionViewItem {
|
||||||
public var clickHandler: (() -> Void)?
|
public var clickHandler: (() -> Void)?
|
||||||
|
@IBOutlet private var peerMarkerView: NSImageView?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@@ -17,10 +18,33 @@ class DeviceListCell: NSCollectionViewItem {
|
|||||||
btn.setButtonType(.momentaryPushIn)
|
btn.setButtonType(.momentaryPushIn)
|
||||||
btn.action = #selector(onClick)
|
btn.action = #selector(onClick)
|
||||||
btn.target = self
|
btn.target = self
|
||||||
|
|
||||||
|
addClickRecognizer(to: view)
|
||||||
|
addClickRecognizer(to: textField)
|
||||||
|
addClickRecognizer(to: imageView)
|
||||||
|
addClickRecognizer(to: peerMarkerView)
|
||||||
|
|
||||||
|
peerMarkerView?.image = NSImage(named: "MenuBarIcon")
|
||||||
|
peerMarkerView?.image?.isTemplate = true
|
||||||
|
peerMarkerView?.contentTintColor = .white
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func onClick(_: Any?) {
|
@IBAction func onClick(_: Any?) {
|
||||||
guard let handler = clickHandler else { return }
|
guard let handler = clickHandler else { return }
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configure(deviceName: String, deviceImage: NSImage, showsQuickDropPeerMarker: Bool) {
|
||||||
|
textField?.stringValue = deviceName
|
||||||
|
imageView?.image = deviceImage
|
||||||
|
peerMarkerView?.isHidden = !showsQuickDropPeerMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addClickRecognizer(to targetView: NSView?) {
|
||||||
|
guard let targetView else { return }
|
||||||
|
|
||||||
|
let recognizer = NSClickGestureRecognizer(target: self, action: #selector(onClick(_:)))
|
||||||
|
recognizer.buttonMask = 0x1
|
||||||
|
targetView.addGestureRecognizer(recognizer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -36,10 +36,20 @@
|
|||||||
</constraints>
|
</constraints>
|
||||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="QGU-rR-rJQ"/>
|
||||||
</imageView>
|
</imageView>
|
||||||
|
<imageView hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="89K-DQ-f8g">
|
||||||
|
<rect key="frame" x="95.799999999999997" y="58.799999999999997" width="14.4" height="14.4"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" constant="14.4" id="Lfh-Ha-Njl"/>
|
||||||
|
<constraint firstAttribute="height" constant="14.4" id="Wm1-YW-85X"/>
|
||||||
|
</constraints>
|
||||||
|
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="0fp-y5-fj9"/>
|
||||||
|
</imageView>
|
||||||
</subviews>
|
</subviews>
|
||||||
<constraints>
|
<constraints>
|
||||||
|
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerX" secondItem="O1J-am-CoK" secondAttribute="centerX" id="Bja-VK-UQf"/>
|
||||||
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
|
<constraint firstItem="O1J-am-CoK" firstAttribute="centerX" secondItem="Hz6-mo-xeY" secondAttribute="centerX" id="R76-2L-pXb"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
|
<constraint firstAttribute="bottom" secondItem="QPM-sE-maP" secondAttribute="bottom" id="Ryn-nY-yZp"/>
|
||||||
|
<constraint firstItem="89K-DQ-f8g" firstAttribute="centerY" secondItem="O1J-am-CoK" secondAttribute="centerY" id="Yeh-mY-trP"/>
|
||||||
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
|
<constraint firstItem="O1J-am-CoK" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="ZFA-S9-fpT"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
|
<constraint firstAttribute="trailing" secondItem="QPM-sE-maP" secondAttribute="trailing" id="eOl-Va-HXq"/>
|
||||||
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
|
<constraint firstItem="QPM-sE-maP" firstAttribute="top" secondItem="O1J-am-CoK" secondAttribute="bottom" constant="10" id="leg-nD-mHB"/>
|
||||||
@@ -53,6 +63,7 @@
|
|||||||
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
|
<collectionViewItem id="1Lb-iJ-Nar" customClass="DeviceListCell" customModule="QuickDropShareExtension" customModuleProvider="target">
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
|
<outlet property="imageView" destination="O1J-am-CoK" id="zQn-mV-bRy"/>
|
||||||
|
<outlet property="peerMarkerView" destination="89K-DQ-f8g" id="iWY-ru-o1h"/>
|
||||||
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
|
<outlet property="textField" destination="QPM-sE-maP" id="s0j-Qq-7iA"/>
|
||||||
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
|
<outlet property="view" destination="Hz6-mo-xeY" id="gZn-20-o77"/>
|
||||||
</connections>
|
</connections>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# Make Finder > Share > QuickDrop use the app Xcode just built (not a stale Release/archive copy).
|
||||||
|
#
|
||||||
|
# macOS LaunchServices registers EVERY QuickDrop.app on disk that shares the bundle id
|
||||||
|
# (com.leonboettger.neardrop) -- Debug build, Release build, archives, old DerivedData, ... -- and the
|
||||||
|
# share sheet then picks among those duplicates heuristically, so a stale copy can run instead of the
|
||||||
|
# one you just built. This script makes the freshly built app the ONLY registered copy of that bundle id.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# - Standalone: scripts/use-dev-share-extension.sh (re-points Finder at the newest Debug build)
|
||||||
|
# - Xcode: add as a Run post-action (Product > Scheme > Edit Scheme > Run > Post-actions >
|
||||||
|
# "+" > New Run Script Action; "Provide build settings from: QuickDrop"; paste:
|
||||||
|
# "${SRCROOT}/scripts/use-dev-share-extension.sh"
|
||||||
|
# Post-actions run outside the build sandbox, so lsregister/mdfind work there even
|
||||||
|
# though ENABLE_USER_SCRIPT_SANDBOXING is YES for build phases.
|
||||||
|
#
|
||||||
|
# `set -u` (not -e): a transient lsregister hiccup shouldn't fail the Xcode Run post-action (red dialog).
|
||||||
|
set -u
|
||||||
|
|
||||||
|
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||||
|
BUNDLE_ID="com.leonboettger.neardrop"
|
||||||
|
|
||||||
|
# Prefer the app Xcode just built (post-action provides these); else the newest Debug build on disk.
|
||||||
|
APP=""
|
||||||
|
if [ "${BUILT_PRODUCTS_DIR:-}" != "" ] && [ "${WRAPPER_NAME:-}" != "" ] && [ -d "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}" ]; then
|
||||||
|
APP="${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}"
|
||||||
|
else
|
||||||
|
APP="$(mdfind "kMDItemCFBundleIdentifier == '${BUNDLE_ID}'" 2>/dev/null | grep '/Build/Products/Debug/QuickDrop.app$' | head -1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${APP}" ] || [ ! -d "${APP}" ]; then
|
||||||
|
echo "use-dev-share-extension: no QuickDrop Debug build found -- build it in Xcode first." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unregister every OTHER on-disk copy of the bundle id, then (re)register only this one.
|
||||||
|
mdfind "kMDItemCFBundleIdentifier == '${BUNDLE_ID}'" 2>/dev/null | while IFS= read -r other; do
|
||||||
|
[ "${other}" = "${APP}" ] && continue
|
||||||
|
"${LSREGISTER}" -u "${other}" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
"${LSREGISTER}" -f "${APP}"
|
||||||
|
|
||||||
|
# Drop any stale running extension so the next Share rebuilds against the fresh registration.
|
||||||
|
pkill -f "${BUNDLE_ID}.ShareExtension" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "use-dev-share-extension: Finder Share -> QuickDrop now points at:"
|
||||||
|
echo " ${APP}"
|
||||||