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 | ||
|
|
0d0b720114 | ||
|
|
13f3073077 | ||
|
|
ff39ba4c7d | ||
|
|
245358ae96 | ||
|
|
429069f372 | ||
|
|
04e056da02 | ||
|
|
50d4d4ee92 | ||
|
|
86b7e4ca24 | ||
|
|
d3a95c068f | ||
|
|
e3cbc19f11 | ||
|
|
c46f6eedbc | ||
|
|
ea213906d6 | ||
|
|
4700a9c56e | ||
|
|
4fe30da52b | ||
|
|
bd93961c59 | ||
|
|
f244572508 | ||
|
|
d4ea143d51 | ||
|
|
2dbc46f792 | ||
|
|
f61be69507 | ||
|
|
82f40d780f | ||
|
|
606082d76d | ||
|
|
2233699ece | ||
|
|
130935c01d | ||
|
|
74226d2fc3 | ||
|
|
e895e7a68e | ||
|
|
843ed9fe4f | ||
|
|
2ef3bc3b44 | ||
|
|
216e2cf851 | ||
|
|
d63665136b | ||
|
|
42c101605f | ||
|
|
fd1dfb4f4a | ||
|
|
767a5c5706 | ||
|
|
8e78f58b5e | ||
|
|
935bdc5a65 | ||
|
|
f559a6ac9d | ||
|
|
e853ba2b36 | ||
|
|
4621395a87 | ||
|
|
55e2a72a90 | ||
|
|
159e83179b | ||
|
|
6a77e4aba6 | ||
|
|
ffe3ac7a4a | ||
|
|
2dbe945f3e | ||
|
|
e828edd6ae | ||
|
|
7fbe254118 | ||
|
|
cffaabc076 | ||
|
|
3736605509 | ||
|
|
e0407d9398 | ||
|
|
3ed93e6079 | ||
|
|
e1924e1919 | ||
|
|
c10facbedd | ||
|
|
a1b4a075a6 | ||
|
|
bf7ad37d15 | ||
|
|
e68167788a | ||
|
|
48eb5be4a5 | ||
|
|
a352e9ed1c | ||
|
|
0f8c9d254e | ||
|
|
be4d342894 | ||
|
|
811b991fcd | ||
|
|
d46b62d3de | ||
|
|
093390084f | ||
|
|
b16556dd1b | ||
|
|
f5d895ed56 | ||
|
|
c489ef018f | ||
|
|
7e0a2e2830 | ||
|
|
0b94a888b5 | ||
|
|
15677b7843 | ||
|
|
6401c1969c | ||
|
|
bb1f28af0b | ||
|
|
d367e0ff34 | ||
|
|
ec093cf302 | ||
|
|
edc09b048c | ||
|
|
9f15ee85f6 | ||
|
|
10ef0c5a21 | ||
|
|
0aa4fd6894 | ||
|
|
71bddf6541 | ||
|
|
3ac2280dc6 | ||
|
|
8b965a7eb2 | ||
|
|
f11943475c | ||
|
|
a5781e6e96 | ||
|
|
ed20c9155e | ||
|
|
b55d6ff07b | ||
|
|
c86ed97bc2 | ||
|
|
eb32603c5f | ||
|
|
044753efb1 | ||
|
|
e8fdd0e94a | ||
|
|
09ef75d249 | ||
|
|
3551c1a006 | ||
|
|
d748167f0e | ||
|
|
02e8af22ab | ||
|
|
0e1fab8faa | ||
|
|
0396c56a31 | ||
|
|
ffdf667cc6 | ||
|
|
cce84e9460 | ||
|
|
b546370e87 | ||
|
|
fbd14c2280 | ||
|
|
bf1a8164d9 | ||
|
|
0a53ad79cb | ||
|
|
800fd23be3 | ||
|
|
487a27a019 | ||
|
|
c74858ea19 | ||
|
|
40169334ff | ||
|
|
ed213b4d3c | ||
|
|
0697b94547 | ||
|
|
e8fc6f719e | ||
|
|
9dfe864677 | ||
|
|
a9e6ed1ddf | ||
|
|
728385ff95 | ||
|
|
46cdec35d5 | ||
|
|
daa1ab77df | ||
|
|
6766ef69d9 | ||
|
|
7a964e35ca | ||
|
|
733ed8bec8 | ||
|
|
24a0202ae9 | ||
|
|
945fdcaf6e | ||
|
|
7cdfdcd474 | ||
|
|
ad1ab30b94 | ||
|
|
4a8ba26c8c | ||
|
|
c887ccb637 | ||
|
|
fb13405247 | ||
|
|
b61aecf315 | ||
|
|
8a9bb0bdfe | ||
|
|
1bd15acaef | ||
|
|
3b99eda41c | ||
|
|
8393a7495f | ||
|
|
a5e1030c1f | ||
|
|
8aa3341595 | ||
|
|
f5e3c88daf | ||
|
|
488f898d7b | ||
|
|
9e7278d327 | ||
|
|
fdc19b6f62 | ||
|
|
734cf766d5 | ||
|
|
f948c30027 | ||
|
|
0c440b7511 | ||
|
|
bf5724ece1 | ||
|
|
658a2ff28e | ||
|
|
4ddf39817c | ||
|
|
3fa1d8621e | ||
|
|
394aaa8093 | ||
|
|
5e62f3e107 | ||
|
|
3f65ace2ae | ||
|
|
41b6eb8e3a | ||
|
|
1256b91d4f | ||
|
|
9fd07e9326 | ||
|
|
70a6b5ff3e | ||
|
|
9536854fa2 | ||
|
|
39ea48fc80 | ||
|
|
4e73fced74 | ||
|
|
21f0f865b6 | ||
|
|
026ebc33db | ||
|
|
d01e1d7c34 | ||
|
|
14390035a3 | ||
|
|
a3d295cd38 | ||
|
|
0ca2a13d66 | ||
|
|
05321adb6d | ||
|
|
31b93a34a2 | ||
|
|
261f91bc94 | ||
|
|
646da767a7 | ||
|
|
9d1e698d9b | ||
|
|
a69a75cb07 | ||
|
|
1ac988a4fc | ||
|
|
673d4418f8 | ||
|
|
779144d26a | ||
|
|
0fa585652d | ||
|
|
1b1206d0b8 | ||
|
|
86afa8a206 | ||
|
|
dda001cb1a | ||
|
|
4ae1663779 | ||
|
|
66acbf25a3 | ||
|
|
93537094e4 | ||
|
|
b6b11c00c0 | ||
|
|
92671d8983 | ||
|
|
bfc23fde15 | ||
|
|
a39a5a324a | ||
|
|
fd7dfd959b | ||
|
|
bd4b181a49 | ||
|
|
f066a11608 | ||
|
|
5429126c1e | ||
|
|
4bf8a9052d | ||
|
|
f786af9d12 | ||
|
|
6def7dbc9f | ||
|
|
fcc0ce0818 | ||
|
|
fd6978cc90 | ||
|
|
12ef87c4da | ||
|
|
518fabcd33 | ||
|
|
eb6bb8fc4d | ||
|
|
d443412697 | ||
|
|
110b0a4ae7 | ||
|
|
2b272c81c1 | ||
|
|
11afc232ab | ||
|
|
943414db58 | ||
|
|
b135229d30 | ||
|
|
6126d9428d |
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
DerivedData/
|
DerivedData/
|
||||||
|
.DerivedData/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
/research/
|
||||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 492 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
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 |
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"appPolicies" : {
|
||||||
|
"eula" : "",
|
||||||
|
"policies" : [
|
||||||
|
{
|
||||||
|
"locale" : "en_US",
|
||||||
|
"policyText" : "",
|
||||||
|
"policyURL" : ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"identifier" : "07114EE9",
|
||||||
|
"nonRenewingSubscriptions" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"products" : [
|
||||||
|
{
|
||||||
|
"displayPrice" : "4.99",
|
||||||
|
"familyShareable" : true,
|
||||||
|
"internalID" : "6742512912",
|
||||||
|
"localizations" : [
|
||||||
|
{
|
||||||
|
"description" : "Unlocks unlimited file transfer, forever.",
|
||||||
|
"displayName" : "QuickDrop+",
|
||||||
|
"locale" : "en_US"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"productID" : "plusversion",
|
||||||
|
"referenceName" : "QuickDrop+ Lifetime",
|
||||||
|
"type" : "NonConsumable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings" : {
|
||||||
|
"_applicationInternalID" : "6740147178",
|
||||||
|
"_askToBuyEnabled" : false,
|
||||||
|
"_billingGracePeriodEnabled" : false,
|
||||||
|
"_billingIssuesEnabled" : false,
|
||||||
|
"_developerTeamID" : "92SDAWLN76",
|
||||||
|
"_disableDialogs" : false,
|
||||||
|
"_failTransactionsEnabled" : false,
|
||||||
|
"_lastSynchronizedDate" : 788014826.59455097,
|
||||||
|
"_locale" : "en_US",
|
||||||
|
"_renewalBillingIssuesEnabled" : false,
|
||||||
|
"_storefront" : "USA",
|
||||||
|
"_storeKitErrors" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"_timeRate" : 0
|
||||||
|
},
|
||||||
|
"subscriptionGroups" : [
|
||||||
|
{
|
||||||
|
"id" : "21845627",
|
||||||
|
"localizations" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"name" : "QuickDrop+ Subscription",
|
||||||
|
"subscriptions" : [
|
||||||
|
{
|
||||||
|
"adHocOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"codeOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"displayPrice" : "1.99",
|
||||||
|
"familyShareable" : false,
|
||||||
|
"groupNumber" : 1,
|
||||||
|
"internalID" : "6756841799",
|
||||||
|
"introductoryOffer" : null,
|
||||||
|
"localizations" : [
|
||||||
|
{
|
||||||
|
"description" : "Unlocks unlimited file transfer.",
|
||||||
|
"displayName" : "QuickDrop+ (1 Month)",
|
||||||
|
"locale" : "en_US"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"productID" : "quickdrop.monthly",
|
||||||
|
"recurringSubscriptionPeriod" : "P1M",
|
||||||
|
"referenceName" : "QuickDrop+ (1 Month)",
|
||||||
|
"subscriptionGroupID" : "21845627",
|
||||||
|
"type" : "RecurringSubscription",
|
||||||
|
"winbackOffers" : [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"adHocOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"codeOffers" : [
|
||||||
|
|
||||||
|
],
|
||||||
|
"displayPrice" : "3.99",
|
||||||
|
"familyShareable" : true,
|
||||||
|
"groupNumber" : 1,
|
||||||
|
"internalID" : "6755912812",
|
||||||
|
"introductoryOffer" : {
|
||||||
|
"internalID" : "D30B2549",
|
||||||
|
"numberOfPeriods" : 1,
|
||||||
|
"paymentMode" : "free",
|
||||||
|
"subscriptionPeriod" : "P3D"
|
||||||
|
},
|
||||||
|
"localizations" : [
|
||||||
|
{
|
||||||
|
"description" : "Unlocks unlimited file transfer.",
|
||||||
|
"displayName" : "QuickDrop+ (1 Year)",
|
||||||
|
"locale" : "en_US"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"productID" : "quickdrop.yearly",
|
||||||
|
"recurringSubscriptionPeriod" : "P1Y",
|
||||||
|
"referenceName" : "QuickDrop+ (1 Year)",
|
||||||
|
"subscriptionGroupID" : "21845627",
|
||||||
|
"type" : "RecurringSubscription",
|
||||||
|
"winbackOffers" : [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : {
|
||||||
|
"major" : 4,
|
||||||
|
"minor" : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import Network
|
|||||||
import System
|
import System
|
||||||
import BigInt
|
import BigInt
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
|
import ASN1
|
||||||
|
import LUI
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
@@ -22,13 +24,20 @@ import UIKit
|
|||||||
class InboundNearbyConnection: NearbyConnection {
|
class InboundNearbyConnection: NearbyConnection {
|
||||||
|
|
||||||
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
private var filesToBeReceived: [Int64: InternalFileInfo] = [:]
|
||||||
|
private var downloadedFiles = [URL]()
|
||||||
private var currentState: State = .initial
|
private var currentState: State = .initial
|
||||||
private var cipherCommitment: Data?
|
private var cipherCommitment: Data?
|
||||||
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 pairingMetadata: Sharing_Nearby_PairingMetadata?
|
||||||
|
private var isPairingSetupRequest = false
|
||||||
var isPlainTextTransfer = false
|
var isPlainTextTransfer = false
|
||||||
|
var isClipboardSyncTransfer = false
|
||||||
|
var isControlTransfer = false
|
||||||
|
|
||||||
var wasUserRejected = false
|
var wasUserRejected = false
|
||||||
var delegate: InboundNearbyConnectionDelegate?
|
var delegate: InboundNearbyConnectionDelegate?
|
||||||
@@ -44,15 +53,22 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
override func disconnect() {
|
override func disconnect() {
|
||||||
|
let recoveredAllPendingFiles = recoverCompletedFilesOnDisconnect()
|
||||||
|
if recoveredAllPendingFiles {
|
||||||
|
if case let .protocolError(message) = self.lastError as? NearbyError,
|
||||||
|
message == "Error.ClosedByPeer".localized() {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Recovering completed transfer after connection closed by peer.")
|
||||||
|
self.lastError = nil
|
||||||
|
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.disconnect()
|
super.disconnect()
|
||||||
currentState = .disconnected
|
currentState = .disconnected
|
||||||
deletePartiallyReceivedFiles()
|
deletePartiallyReceivedFiles()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.connectionWasTerminated(connection: self, error: self.lastError)
|
self.delegate?.connectionWasTerminated(connection: self, savedFiles: self.downloadedFiles, error: self.lastError)
|
||||||
|
|
||||||
SaveFilesManager.shared.movePendingFilesToTarget()
|
|
||||||
SaveFilesManager.shared.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +79,19 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
log("[InboundNearbyConnection \(self.id)] Received frame in state \(currentState)...")
|
log("[InboundNearbyConnection \(self.id)] Received frame in state \(currentState)...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !encryptionDone,
|
||||||
|
currentState != .initial,
|
||||||
|
let frame = try? Location_Nearby_Connections_OfflineFrame(serializedBytes: frameData),
|
||||||
|
frame.hasV1,
|
||||||
|
frame.v1.hasType,
|
||||||
|
case .keepAlive = frame.v1.type {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received keep-alive during handshake in state \(currentState)")
|
||||||
|
if !frame.v1.hasKeepAlive || !frame.v1.keepAlive.ack {
|
||||||
|
sendKeepAlive(ack: true)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .initial:
|
case .initial:
|
||||||
@@ -112,11 +141,64 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .sentConnectionResponse:
|
case .sentConnectionResponse:
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyEncryption {
|
||||||
try processPairedKeyEncryptionFrame(frame)
|
try processPairedKeyEncryptionFrame(frame)
|
||||||
case .sentPairedKeyResult:
|
return
|
||||||
try processPairedKeyResultFrame(frame)
|
}
|
||||||
case .receivedPairedKeyResult:
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received introduction before paired-key exchange finished; continuing unauthenticated flow.")
|
||||||
|
try sendPairedKeyResult(status: .unable)
|
||||||
try processIntroductionFrame(frame)
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyResult || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyEncryption")
|
||||||
|
case .sentPairedKeyResult:
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
try processPairedKeyResultFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Received introduction without pairedKeyResult; continuing for legacy compatibility.")
|
||||||
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.pairedKeyResult")
|
||||||
|
case .receivedPairedKeyResult:
|
||||||
|
if frame.hasV1, frame.v1.hasIntroduction {
|
||||||
|
try processIntroductionFrame(frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyResult || frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for introduction.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.v1.type == .response, case .accept = frame.v1.connectionResponse.status {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Ignoring accept response while waiting for introduction.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("shareNearbyFrame.v1.introduction")
|
||||||
default:
|
default:
|
||||||
if frame.hasV1, frame.v1.hasType, frame.v1.type == .progressUpdate {
|
if frame.hasV1, frame.v1.hasType, frame.v1.type == .progressUpdate {
|
||||||
// ignore progress updates
|
// ignore progress updates
|
||||||
@@ -141,7 +223,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
let id = frame.payloadHeader.id
|
let id = frame.payloadHeader.id
|
||||||
|
|
||||||
guard let fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
|
guard var fileInfo = filesToBeReceived[id] else { throw NearbyError.protocolError("File payload ID \(id) is not known") }
|
||||||
|
|
||||||
let currentOffset = fileInfo.bytesTransferred
|
let currentOffset = fileInfo.bytesTransferred
|
||||||
|
|
||||||
@@ -149,11 +231,15 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
|
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") }
|
||||||
|
|
||||||
if !frame.payloadChunk.body.isEmpty {
|
let hasBody = !frame.payloadChunk.body.isEmpty
|
||||||
|
let isLastChunk = (frame.payloadChunk.flags & 1) == 1
|
||||||
|
|
||||||
|
if hasBody {
|
||||||
do {
|
do {
|
||||||
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
|
try fileInfo.fileHandle?.write(contentsOf: frame.payloadChunk.body)
|
||||||
filesToBeReceived[id]!.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
fileInfo.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
||||||
fileInfo.progress?.completedUnitCount = filesToBeReceived[id]!.bytesTransferred
|
fileInfo.progress?.completedUnitCount = fileInfo.bytesTransferred
|
||||||
|
filesToBeReceived[id] = fileInfo
|
||||||
|
|
||||||
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
self.bytesTransferred += Int64(frame.payloadChunk.body.count)
|
||||||
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
|
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: Double(self.bytesTransferred) / Double(self.bytesToBeTransferred))
|
||||||
@@ -163,14 +249,19 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
throw NearbyError.protocolError(error.localizedDescription)
|
throw NearbyError.protocolError(error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (frame.payloadChunk.flags & 1) == 1 {
|
|
||||||
|
if isLastChunk {
|
||||||
|
guard fileInfo.bytesTransferred == fileInfo.meta.size else {
|
||||||
|
throw NearbyError.protocolError("Received EOF before file was fully transferred (\(fileInfo.bytesTransferred)/\(fileInfo.meta.size) bytes)")
|
||||||
|
}
|
||||||
try fileInfo.fileHandle?.close()
|
try fileInfo.fileHandle?.close()
|
||||||
filesToBeReceived[id]!.fileHandle = nil
|
fileInfo.fileHandle = nil
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
|
||||||
|
filesToBeReceived[id] = fileInfo
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
@@ -179,7 +270,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
try sendDisconnectionAndDisconnect()
|
try sendDisconnectionAndDisconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if !hasBody {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file chunk with no body and no flags, ignoring it.")
|
log("[InboundNearbyConnection \(self.id)] Received file chunk with no body and no flags, ignoring it.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,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
|
||||||
@@ -222,7 +324,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
fileInfo.progress?.unpublish()
|
fileInfo.progress?.unpublish()
|
||||||
#endif
|
#endif
|
||||||
filesToBeReceived.removeValue(forKey: id)
|
filesToBeReceived.removeValue(forKey: id)
|
||||||
SaveFilesManager.shared.registerFileFinishedDownloading(fileInfo.destinationURL)
|
downloadedFiles.append(fileInfo.destinationURL)
|
||||||
|
EXIFUtils.applyTimestamps(at: fileInfo.destinationURL)
|
||||||
|
|
||||||
if filesToBeReceived.isEmpty {
|
if filesToBeReceived.isEmpty {
|
||||||
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
log("[InboundNearbyConnection \(self.id)] Received file payload. Disconnecting...")
|
||||||
@@ -241,19 +344,12 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
guard case .connectionRequest = frame.v1.type else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
||||||
|
|
||||||
let endpointInfo = frame.v1.connectionRequest.endpointInfo
|
let endpointInfo = EndpointInfo(data: frame.v1.connectionRequest.endpointInfo)
|
||||||
|
let fallbackDeviceName = "AndroidDevice".localized()
|
||||||
|
let resolvedDeviceName = endpointInfo?.name?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let deviceName = (resolvedDeviceName?.isEmpty == false) ? resolvedDeviceName : fallbackDeviceName
|
||||||
|
|
||||||
guard endpointInfo.count > 17 else { throw NearbyError.protocolError("Endpoint info too short") }
|
remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: endpointInfo?.deviceType)
|
||||||
|
|
||||||
let deviceNameLength = Int(endpointInfo[17])
|
|
||||||
|
|
||||||
guard endpointInfo.count >= deviceNameLength + 18 else { throw NearbyError.protocolError("Endpoint info too short to contain the device name") }
|
|
||||||
|
|
||||||
guard let deviceName = String(data: endpointInfo[18 ..< (18 + deviceNameLength)], encoding: .utf8) else { throw NearbyError.protocolError("Device name is not valid UTF-8") }
|
|
||||||
|
|
||||||
let rawDeviceType = Int(endpointInfo[0] & 7) >> 1
|
|
||||||
|
|
||||||
remoteDeviceInfo = RemoteDeviceInfo(name: deviceName, type: RemoteDeviceInfo.DeviceType.fromRawValue(value: rawDeviceType))
|
|
||||||
currentState = .receivedConnectionRequest
|
currentState = .receivedConnectionRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,9 +470,28 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
pairedEncryption.v1.type = .pairedKeyEncryption
|
pairedEncryption.v1.type = .pairedKeyEncryption
|
||||||
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
|
pairedEncryption.v1.pairedKeyEncryption = Sharing_Nearby_PairedKeyEncryptionFrame()
|
||||||
|
|
||||||
// For inbound connections, we do not use provide secretIDHash and signedData
|
if let signingPrivateKey = IdentityManager.shared.getPrivateKey(),
|
||||||
|
let publicKey = IdentityManager.shared.getPublicKey()?.toGenericPublicKey(),
|
||||||
|
let publicKeyData = IdentityManager.shared.getPublicKey()?.toGenericPublicKeyData(),
|
||||||
|
let publicKeyID = publicKey.id(),
|
||||||
|
let authKeyData = self.authKey?.data() {
|
||||||
|
|
||||||
|
var cert = Sharing_Nearby_PublicCertificate()
|
||||||
|
cert.secretID = publicKeyID
|
||||||
|
cert.publicKey = publicKeyData
|
||||||
|
|
||||||
|
let signatureTuple = signingPrivateKey.sign(msg: authKeyData)
|
||||||
|
|
||||||
|
pairedEncryption.v1.certificateInfo.publicCertificate.append(cert)
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = cert.secretID
|
||||||
|
pairedEncryption.v1.pairedKeyEncryption.signedData = Data(signatureTuple.asn1.encode())
|
||||||
|
|
||||||
|
pairedEncryption.v1.pairedKeyResult.status = .success
|
||||||
|
} else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] No private key available for receiver authentication.")
|
||||||
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
|
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6)
|
||||||
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
|
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72)
|
||||||
|
}
|
||||||
|
|
||||||
try sendTransferSetupFrame(pairedEncryption)
|
try sendTransferSetupFrame(pairedEncryption)
|
||||||
currentState = .sentConnectionResponse
|
currentState = .sentConnectionResponse
|
||||||
@@ -392,6 +507,13 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
let pkeFrame = frame.v1.pairedKeyEncryption
|
let pkeFrame = frame.v1.pairedKeyEncryption
|
||||||
|
|
||||||
|
if frame.v1.certificateInfo.publicCertificate.isEmpty {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Detected legacy peer.")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Detected QuickDrop peer.")
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we can accept the connection automatically
|
// Check if we can accept the connection automatically
|
||||||
if !pkeFrame.secretIDHash.isEmpty,
|
if !pkeFrame.secretIDHash.isEmpty,
|
||||||
let trustedCertData = TrustStore.shared.findTrustedKey(for: pkeFrame.secretIDHash),
|
let trustedCertData = TrustStore.shared.findTrustedKey(for: pkeFrame.secretIDHash),
|
||||||
@@ -416,6 +538,7 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
if peerPublicKey.verify(signature: signature, msg: authKeyData) {
|
if peerPublicKey.verify(signature: signature, msg: authKeyData) {
|
||||||
log("[InboundNearbyConnection \(self.id)] Paired key authentication successful.")
|
log("[InboundNearbyConnection \(self.id)] Paired key authentication successful.")
|
||||||
|
self.peerCertificate = trustedCert
|
||||||
try sendPairedKeyResult(status: .success)
|
try sendPairedKeyResult(status: .success)
|
||||||
isAuthenticated = true
|
isAuthenticated = true
|
||||||
} else {
|
} else {
|
||||||
@@ -457,13 +580,16 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func makeFileDestinationURL(_ initialDest: URL) -> URL {
|
private func makeFileDestinationURL(_ initialDest: URL, usedDestinations: Set<URL>) -> URL {
|
||||||
var dest = initialDest
|
var dest = initialDest
|
||||||
if FileManager.default.fileExists(atPath: dest.path) {
|
let fm = FileManager.default
|
||||||
|
|
||||||
|
if fm.fileExists(atPath: dest.path) || usedDestinations.contains(dest) {
|
||||||
var counter = 1
|
var counter = 1
|
||||||
var path: String
|
var path: String
|
||||||
let ext = dest.pathExtension
|
let ext = dest.pathExtension
|
||||||
@@ -474,8 +600,8 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
path += ".\(ext)"
|
path += ".\(ext)"
|
||||||
}
|
}
|
||||||
counter += 1
|
counter += 1
|
||||||
} while FileManager.default.fileExists(atPath: path)
|
|
||||||
dest = URL(fileURLWithPath: path)
|
dest = URL(fileURLWithPath: path)
|
||||||
|
} while fm.fileExists(atPath: dest.path) || usedDestinations.contains(dest)
|
||||||
}
|
}
|
||||||
return dest
|
return dest
|
||||||
}
|
}
|
||||||
@@ -486,55 +612,184 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
currentState = .waitingForUserConsent
|
currentState = .waitingForUserConsent
|
||||||
|
|
||||||
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
if frame.v1.introduction.fileMetadata.count > 0 && frame.v1.introduction.textMetadata.isEmpty {
|
||||||
let saveDirectory = SaveFilesManager.shared.getSaveDirectory()
|
let saveDirectory = NearbyConnectionManager.shared.getSaveDirectory()
|
||||||
|
var usedDestinations = Set<URL>()
|
||||||
|
|
||||||
for file in frame.v1.introduction.fileMetadata {
|
for file in frame.v1.introduction.fileMetadata {
|
||||||
let dest = makeFileDestinationURL(saveDirectory.appendingPathComponent(file.name))
|
let initialDest = saveDirectory.appendingPathComponent(file.name)
|
||||||
let info = InternalFileInfo(meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
|
let dest = makeFileDestinationURL(initialDest, usedDestinations: usedDestinations)
|
||||||
|
usedDestinations.insert(dest)
|
||||||
|
|
||||||
|
let info = InternalFileInfo(
|
||||||
|
meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
|
||||||
payloadID: file.payloadID,
|
payloadID: file.payloadID,
|
||||||
destinationURL: dest)
|
destinationURL: dest
|
||||||
|
)
|
||||||
filesToBeReceived[file.payloadID] = info
|
filesToBeReceived[file.payloadID] = info
|
||||||
bytesToBeTransferred += file.size
|
bytesToBeTransferred += file.size
|
||||||
}
|
}
|
||||||
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
let metadata = TransferMetadata(files: filesToBeReceived.map { $0.value.meta }, id: id, pinCode: pinCode, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
else if let textMetadata = frame.v1.introduction.textMetadata.first {
|
|
||||||
|
|
||||||
if textMetadata.type == .url || textMetadata.type == .text {
|
#if os(macOS)
|
||||||
|
if frame.v1.introduction.hasMirroredNotificationMetadata || frame.v1.introduction.hasPairingMetadata {
|
||||||
|
isControlTransfer = true
|
||||||
|
mirroredNotificationMetadata = frame.v1.introduction.hasMirroredNotificationMetadata
|
||||||
|
? frame.v1.introduction.mirroredNotificationMetadata
|
||||||
|
: nil
|
||||||
|
pairingMetadata = frame.v1.introduction.hasPairingMetadata
|
||||||
|
? frame.v1.introduction.pairingMetadata
|
||||||
|
: nil
|
||||||
|
isPairingSetupRequest = pairingMetadata?.setupRequest == true
|
||||||
|
let hasPairingConfirmation = pairingMetadata?.hasSetupPinHash == true
|
||||||
|
let pairingUseCase = pairingMetadata.flatMap { PairingUseCase(protoValue: $0.useCase) }
|
||||||
|
|
||||||
let isClipboardText = textMetadata.type == .text
|
guard let device = self.remoteDeviceInfo else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
if pairingMetadata != nil && pairingUseCase == nil {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if pairingMetadata != nil,
|
||||||
|
mirroredNotificationMetadata == nil,
|
||||||
|
!isPairingSetupRequest,
|
||||||
|
!hasPairingConfirmation {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAuthenticated && !isPairingSetupRequest && !hasPairingConfirmation {
|
||||||
|
self.lastError = NearbyError.notificationSyncNotTrusted
|
||||||
|
self.rejectTransfer(with: .reject, markAsUserRejected: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPairingConfirmation {
|
||||||
|
guard let peerCertificate = peerCertificate,
|
||||||
|
let pairingMetadata,
|
||||||
|
let pairingUseCase else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
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)
|
||||||
|
if let trustedData {
|
||||||
|
guard let currentData = try? peerCertificate.serializedData(),
|
||||||
|
currentData == trustedData else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
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(
|
||||||
|
secretIdHex: peerCertificate.secretID.hex,
|
||||||
|
useCase: pairingUseCase,
|
||||||
|
pinHash: pairingMetadata.setupPinHash
|
||||||
|
)
|
||||||
|
|
||||||
|
guard matches else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
self.delegate?.pairingSetupConfirmed(connection: self)
|
||||||
|
}
|
||||||
|
submitUserConsent(accepted: true, trustDevice: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAuthenticated || isPairingSetupRequest {
|
||||||
|
guard let transferType = pairingTransferType(for: pairingUseCase) else {
|
||||||
|
self.rejectTransfer(with: .reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let metadata = TransferMetadata(
|
||||||
|
files: [],
|
||||||
|
id: id,
|
||||||
|
pinCode: pinCode,
|
||||||
|
transferType: transferType,
|
||||||
|
allowsToBeAddedAsTrustedDevice: true
|
||||||
|
)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.obtainUserConsent(transfer: metadata, device: device, connection: self)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitUserConsent(accepted: true, trustDevice: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if let textMetadata = frame.v1.introduction.textMetadata.first {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
if isClipboardText {
|
let metadata = TransferMetadata(
|
||||||
|
files: [],
|
||||||
|
id: id,
|
||||||
|
pinCode: pinCode,
|
||||||
|
textDescription: textMetadata.textTitle,
|
||||||
|
transferType: isURL ? .url : .text,
|
||||||
|
allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil,
|
||||||
|
isClipboardSync: isClipboardSyncTransfer
|
||||||
|
)
|
||||||
|
|
||||||
|
if !isURL {
|
||||||
isPlainTextTransfer = true
|
isPlainTextTransfer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfCanProceed(metadata: metadata)
|
checkIfCanProceed(metadata: metadata)
|
||||||
}
|
}
|
||||||
|
else if let wifiMetadata = frame.v1.introduction.wifiCredentialsMetadata.first {
|
||||||
|
|
||||||
|
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: wifiMetadata.ssid, transferType: .wifiPassword, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil)
|
||||||
|
textPayloadID = wifiMetadata.payloadID
|
||||||
|
|
||||||
|
isPlainTextTransfer = true
|
||||||
|
|
||||||
|
checkIfCanProceed(metadata: metadata)
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
rejectDueToUnsupportedFileType(frame)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rejectDueToUnsupportedFileType(frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func checkIfCanProceed(metadata: TransferMetadata) {
|
|
||||||
|
|
||||||
let acceptAutomatically = isAuthenticated || UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue)
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self, acceptAutomatically: acceptAutomatically)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) {
|
|
||||||
|
|
||||||
log("[InboundNearbyConnection \(self.id)] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
|
log("[InboundNearbyConnection \(self.id)] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
|
||||||
|
|
||||||
lastError = NearbyError.canceled(reason: .unsupportedType)
|
lastError = NearbyError.canceled(reason: .unsupportedType)
|
||||||
@@ -542,14 +797,52 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func submitUserConsent(accepted: Bool, trustDevice: Bool, storeInTemp: Bool = false) {
|
func checkIfCanProceed(metadata: TransferMetadata) {
|
||||||
|
|
||||||
|
let acceptAutomatically = isAuthenticated || Settings.sharedInstance.automaticallyAcceptFiles
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if acceptAutomatically {
|
||||||
|
self.delegate?.obtainedUserConsentAutomatically(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
|
||||||
|
self.submitUserConsent(accepted: true, trustDevice: false)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func submitUserConsent(accepted: Bool, trustDevice: Bool, pairingToken: String? = nil) {
|
||||||
|
if isPairingSetupRequest,
|
||||||
|
let peerCertificate = peerCertificate,
|
||||||
|
let pairingMetadata,
|
||||||
|
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
|
||||||
|
let secretIdHex = peerCertificate.secretID.hex
|
||||||
|
if accepted {
|
||||||
|
if let token = pairingToken {
|
||||||
|
TrustStore.shared.registerPendingPairingTrust(
|
||||||
|
secretIdHex: secretIdHex,
|
||||||
|
pinCode: token,
|
||||||
|
useCase: pairingUseCase
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
|
||||||
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
|
if trustDevice, let peerCertificate = peerCertificate, let remoteDeviceInfo = remoteDeviceInfo {
|
||||||
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
TrustStore.shared.addTrusted(certificate: peerCertificate, device: remoteDeviceInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .utility).async {
|
NearbyConnection.dispatchQueue.async {
|
||||||
if accepted {
|
if accepted {
|
||||||
self.acceptTransfer(storeInTemp: storeInTemp)
|
self.acceptTransfer()
|
||||||
} else {
|
} else {
|
||||||
self.rejectTransfer()
|
self.rejectTransfer()
|
||||||
}
|
}
|
||||||
@@ -557,19 +850,28 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func acceptTransfer(storeInTemp: Bool) {
|
private func acceptTransfer() {
|
||||||
if currentState == .disconnected {
|
if currentState == .disconnected {
|
||||||
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
|
log("[InboundNearbyConnection \(self.id)] Detected timeout, not accepting transfer")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isControlTransfer && isFileTransferRestricted() {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] File transfer restrictions detected.")
|
||||||
|
delegate?.showPlusScreen()
|
||||||
|
rejectTransfer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
if storeInTemp {
|
|
||||||
try FileManager.default.createDirectory(at: SaveFilesManager.shared.tempDirectory, withIntermediateDirectories: true)
|
if filesToBeReceived.count >= 1 {
|
||||||
|
// Show progress bar for file transfers immediately
|
||||||
|
NearbyConnectionManager.shared.updatedTransferProgress(connection: self, progress: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, file) in filesToBeReceived {
|
for (id, file) in filesToBeReceived {
|
||||||
let targetURL = storeInTemp ? SaveFilesManager.shared.tempDirectory.appendingPathComponent(file.destinationURL.lastPathComponent) : file.destinationURL
|
let targetURL = file.destinationURL
|
||||||
|
|
||||||
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
FileManager.default.createFile(atPath: targetURL.path, contents: nil)
|
||||||
let handle = try FileHandle(forWritingTo: targetURL)
|
let handle = try FileHandle(forWritingTo: targetURL)
|
||||||
@@ -598,17 +900,36 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
currentState = .receivingFiles
|
currentState = .receivingFiles
|
||||||
isTransferring = true
|
isTransferring = true
|
||||||
try sendTransferSetupFrame(frame)
|
try sendTransferSetupFrame(frame)
|
||||||
|
|
||||||
|
if let mirroredNotificationMetadata,
|
||||||
|
!isPairingSetupRequest {
|
||||||
|
MirroredNotificationPresenter.shared.present(metadata: mirroredNotificationMetadata, senderDeviceName: remoteDeviceInfo?.name)
|
||||||
|
}
|
||||||
|
if isControlTransfer {
|
||||||
|
try sendDisconnectionAndDisconnect()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
lastError = error
|
lastError = error
|
||||||
protocolError()
|
protocolError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pairingTransferType(for useCase: PairingUseCase?) -> TransferMetadata.TransferType? {
|
||||||
|
switch useCase {
|
||||||
|
case .notificationSync:
|
||||||
|
return .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
return .clipboardSync
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject) {
|
|
||||||
|
private func rejectTransfer(with reason: Sharing_Nearby_ConnectionResponseFrame.Status = .reject, markAsUserRejected: Bool = true) {
|
||||||
|
|
||||||
// rejected by user
|
// rejected by user
|
||||||
if reason == .reject {
|
if reason == .reject && markAsUserRejected {
|
||||||
self.wasUserRejected = true
|
self.wasUserRejected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,12 +959,44 @@ class InboundNearbyConnection: NearbyConnection {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
guard file.created else { continue }
|
guard file.created else { continue }
|
||||||
|
try? FileManager.default.removeItem(at: file.destinationURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func recoverCompletedFilesOnDisconnect() -> Bool {
|
||||||
|
guard !filesToBeReceived.isEmpty else { return false }
|
||||||
|
|
||||||
|
var recoveredCount = 0
|
||||||
|
let pendingFileIDs = Array(filesToBeReceived.keys)
|
||||||
|
|
||||||
|
for fileID in pendingFileIDs {
|
||||||
|
guard let file = filesToBeReceived[fileID] else { continue }
|
||||||
|
guard file.created, file.bytesTransferred == file.meta.size else { continue }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(at: file.destinationURL)
|
try file.fileHandle?.close()
|
||||||
}
|
} catch {
|
||||||
catch {
|
log("[InboundNearbyConnection \(self.id)] Failed to close recovered file handle for \(file.destinationURL.lastPathComponent): \(error.localizedDescription)")
|
||||||
// if it fails, we don't care. Could be because file was not created yet
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
file.progress?.unpublish()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if !downloadedFiles.contains(file.destinationURL) {
|
||||||
|
downloadedFiles.append(file.destinationURL)
|
||||||
|
}
|
||||||
|
EXIFUtils.applyTimestamps(at: file.destinationURL)
|
||||||
|
|
||||||
|
filesToBeReceived.removeValue(forKey: fileID)
|
||||||
|
recoveredCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if recoveredCount > 0 {
|
||||||
|
log("[InboundNearbyConnection \(self.id)] Recovered \(recoveredCount) completed file(s) during disconnect.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return recoveredCount > 0 && filesToBeReceived.isEmpty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// MirroredNotificationSupport.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
import UserNotifications
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
final class MirroredNotificationPresenter {
|
||||||
|
|
||||||
|
static let shared = MirroredNotificationPresenter()
|
||||||
|
|
||||||
|
private let center = UNUserNotificationCenter.current()
|
||||||
|
private let queue = DispatchQueue(label: "MirroredNotificationPresenter")
|
||||||
|
private var pendingNotifications: [(Sharing_Nearby_MirroredNotificationMetadata, String?)] = []
|
||||||
|
private var authorizationRequestInFlight = false
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func present(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
queue.async {
|
||||||
|
self.postNotificationIfAuthorized(metadata: metadata, senderDeviceName: senderDeviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postNotificationIfAuthorized(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
center.getNotificationSettings { settings in
|
||||||
|
self.queue.async {
|
||||||
|
self.handleAuthorizationStatus(
|
||||||
|
settings.authorizationStatus,
|
||||||
|
metadata: metadata,
|
||||||
|
senderDeviceName: senderDeviceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthorizationStatus(
|
||||||
|
_ status: UNAuthorizationStatus,
|
||||||
|
metadata: Sharing_Nearby_MirroredNotificationMetadata,
|
||||||
|
senderDeviceName: String?
|
||||||
|
) {
|
||||||
|
switch status {
|
||||||
|
case .authorized, .provisional, .ephemeral:
|
||||||
|
post(metadata: metadata, senderDeviceName: senderDeviceName)
|
||||||
|
case .notDetermined:
|
||||||
|
pendingNotifications.append((metadata, senderDeviceName))
|
||||||
|
requestAuthorizationIfNeeded()
|
||||||
|
case .denied:
|
||||||
|
log("[MirroredNotificationPresenter] Notifications are denied in system settings; cannot mirror.")
|
||||||
|
@unknown default:
|
||||||
|
log("[MirroredNotificationPresenter] Unknown notification authorization status; skipping mirrored notification.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAuthorizationIfNeeded() {
|
||||||
|
if authorizationRequestInFlight {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationRequestInFlight = true
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
|
||||||
|
self.queue.async {
|
||||||
|
self.authorizationRequestInFlight = false
|
||||||
|
|
||||||
|
if let error = error {
|
||||||
|
log("[MirroredNotificationPresenter] Authorization request failed: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard granted else {
|
||||||
|
log("[MirroredNotificationPresenter] Authorization request not granted; dropping queued mirrored notifications.")
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let queued = self.pendingNotifications
|
||||||
|
self.pendingNotifications.removeAll()
|
||||||
|
for (queuedMetadata, queuedSender) in queued {
|
||||||
|
self.post(metadata: queuedMetadata, senderDeviceName: queuedSender)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post(metadata: Sharing_Nearby_MirroredNotificationMetadata, senderDeviceName: String?) {
|
||||||
|
let senderName = senderDeviceName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let hasSenderName = senderName?.isEmpty == false
|
||||||
|
let displaySender = hasSenderName ? senderName! : "NotificationSyncFallbackSenderAndroid".localized()
|
||||||
|
let identifierSender = hasSenderName ? senderName! : "android"
|
||||||
|
let cleanAppName = metadata.appName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanTitle = metadata.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let cleanBody = metadata.body.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let notificationKey = metadata.notificationKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = cleanTitle.isEmpty ? (cleanAppName.isEmpty ? "NotificationSyncFallbackTitle".localized() : cleanAppName) : cleanTitle
|
||||||
|
content.body = cleanBody
|
||||||
|
content.subtitle = cleanAppName.isEmpty ? displaySender : "\(cleanAppName) · \(displaySender)"
|
||||||
|
content.sound = .default
|
||||||
|
content.threadIdentifier = "quickdrop-mirror-\(sanitizeIdentifierComponent(identifierSender))"
|
||||||
|
|
||||||
|
let identifier = makeIdentifier(sender: identifierSender, notificationKey: notificationKey)
|
||||||
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||||
|
center.add(request) { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[MirroredNotificationPresenter] Could not post notification: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
Settings.sharedInstance.incomingNotificationCount += 1
|
||||||
|
let count = Settings.sharedInstance.incomingNotificationCount
|
||||||
|
log("[MirroredNotificationPresenter] Posted mirrored notification (\(identifier)). Current count: \(count)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeIdentifier(sender: String, notificationKey: String) -> String {
|
||||||
|
let raw = "\(sender)|\(notificationKey)"
|
||||||
|
let digest = SHA256.hash(data: Data(raw.utf8))
|
||||||
|
let prefix = digest.prefix(12).map { String(format: "%02x", $0) }.joined()
|
||||||
|
return "quickdrop-mirror-\(prefix)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sanitizeIdentifierComponent(_ value: String) -> String {
|
||||||
|
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
|
||||||
|
let scalarView = value.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
|
||||||
|
let sanitized = String(scalarView)
|
||||||
|
return sanitized.isEmpty ? "android" : sanitized
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,11 +12,12 @@ import Network
|
|||||||
import System
|
import System
|
||||||
import BigInt
|
import BigInt
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
|
import LUI
|
||||||
|
|
||||||
class NearbyConnection {
|
class NearbyConnection {
|
||||||
|
|
||||||
private let maxFrameLength = 5 * 1024 * 1024
|
private let maxFrameLength = 5 * 1024 * 1024
|
||||||
private static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility)
|
static let dispatchQueue = DispatchQueue(label: "com.leonboettger.quickdrop.queue", qos: .utility)
|
||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
let connection: NWConnection
|
let connection: NWConnection
|
||||||
@@ -28,16 +29,25 @@ class NearbyConnection {
|
|||||||
var cancelled: Bool = false
|
var cancelled: Bool = false
|
||||||
var isTransferring: Bool = false {
|
var isTransferring: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
|
dataTransferInactivityTimer?.cancel()
|
||||||
|
dataTransferInactivityTimer = nil
|
||||||
|
|
||||||
|
if isTransferring {
|
||||||
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.")
|
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.")
|
||||||
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
|
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var payloadBuffers: [Int64: NSMutableData] = [:]
|
private var payloadBuffers: [Int64: NSMutableData] = [:]
|
||||||
private var connectionClosed: Bool = false
|
private var connectionClosed: Bool = false
|
||||||
|
|
||||||
private var inactivityTimer: DispatchSourceTimer?
|
private var inactivityTimer: DispatchSourceTimer?
|
||||||
private let timeoutInterval: TimeInterval = 30
|
private var dataTransferInactivityTimer: DispatchSourceTimer?
|
||||||
|
|
||||||
|
private let ackInterval: TimeInterval = 5
|
||||||
|
private let ackTimeoutInterval: TimeInterval = 60
|
||||||
|
private var lastKeepAliveFrameSent = Date()
|
||||||
|
|
||||||
// UKEY2-related state
|
// UKEY2-related state
|
||||||
var privateKey: ECPrivateKey?
|
var privateKey: ECPrivateKey?
|
||||||
@@ -117,7 +127,7 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
func connectionClosedByPeer(onError: @escaping () -> Void) {
|
func connectionClosedByPeer(onError: @escaping () -> Void) {
|
||||||
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + 0.5) {
|
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + 1) {
|
||||||
|
|
||||||
// If already closed, ignore the error, as file transfer has been processed
|
// If already closed, ignore the error, as file transfer has been processed
|
||||||
if !self.connectionClosed {
|
if !self.connectionClosed {
|
||||||
@@ -164,6 +174,8 @@ class NearbyConnection {
|
|||||||
private func receiveFrameAsync() {
|
private func receiveFrameAsync() {
|
||||||
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
|
connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { content, _, isComplete, error in
|
||||||
if self.connectionClosed {
|
if self.connectionClosed {
|
||||||
|
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||||
|
self.connection.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -171,6 +183,7 @@ class NearbyConnection {
|
|||||||
self.connectionClosedByPeer {
|
self.connectionClosedByPeer {
|
||||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
self.connection.cancel()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -182,7 +195,14 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
guard let content = content else {
|
guard let content = content else {
|
||||||
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(). IsComplete: \(isComplete)")
|
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(). IsComplete: \(isComplete)")
|
||||||
assertionFailure()
|
self.lastError = NearbyError.protocolError("Received empty frame header")
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard content.count == 4 else {
|
||||||
|
log("[NearbyConnection \(self.id)] Received invalid frame header size \(content.count), expected 4")
|
||||||
|
self.lastError = NearbyError.protocolError("Unexpected frame header length")
|
||||||
|
self.protocolError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
|
let frameLength = UInt32(content[0]) << 24 | UInt32(content[1]) << 16 | UInt32(content[2]) << 8 | UInt32(content[3])
|
||||||
@@ -199,8 +219,10 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
private func receiveFrameAsync(length: UInt32) {
|
private func receiveFrameAsync(length: UInt32) {
|
||||||
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, _ in
|
connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { [self] content, _, isComplete, error in
|
||||||
if self.connectionClosed {
|
if self.connectionClosed {
|
||||||
|
log("[NearbyConnection \(self.id)] Received FIN from peer, connection closed.")
|
||||||
|
connection.cancel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isComplete {
|
if isComplete {
|
||||||
@@ -208,6 +230,13 @@ class NearbyConnection {
|
|||||||
|
|
||||||
if let content = content, !content.isEmpty {
|
if let content = content, !content.isEmpty {
|
||||||
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), and frame not empty. Frame length: \(content.count)")
|
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), and frame not empty. Frame length: \(content.count)")
|
||||||
|
|
||||||
|
if content.count == Int(length) {
|
||||||
|
self.processReceivedFrame(frameData: content)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log("[NearbyConnection \(self.id)] Dropping trailing partial frame during connection close. Expected \(length) bytes, got \(content.count)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), but no content received anymore (\(String(describing: content))).")
|
log("[NearbyConnection \(self.id)] Connection closed by peer during receiveFrameAsync(length:), but no content received anymore (\(String(describing: content))).")
|
||||||
@@ -216,15 +245,28 @@ class NearbyConnection {
|
|||||||
self.connectionClosedByPeer {
|
self.connectionClosedByPeer {
|
||||||
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
self.lastError = NearbyError.protocolError("Error.ClosedByPeer".localized())
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
self.connection.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if let error = error {
|
||||||
|
log("[NearbyConnection \(self.id)] Error during receiveFrameAsync(length:): \(error)")
|
||||||
|
self.lastError = error
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
guard let content = content else {
|
guard let content = content else {
|
||||||
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(length:). IsComplete: \(isComplete)")
|
log("[NearbyConnection \(self.id)] Received nil content during receiveFrameAsync(length:). IsComplete: \(isComplete)")
|
||||||
self.protocolError()
|
self.protocolError()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard content.count == Int(length) else {
|
||||||
|
log("[NearbyConnection \(self.id)] Dropping partial frame body. Expected \(length) bytes, got \(content.count)")
|
||||||
|
self.lastError = NearbyError.protocolError("Unexpected frame body length")
|
||||||
|
self.protocolError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.startAndResetHeartbeatTimer()
|
self.startAndResetHeartbeatTimer()
|
||||||
self.processReceivedFrame(frameData: content)
|
self.processReceivedFrame(frameData: content)
|
||||||
@@ -246,7 +288,10 @@ class NearbyConnection {
|
|||||||
UInt8(truncatingIfNeeded: length),
|
UInt8(truncatingIfNeeded: length),
|
||||||
])
|
])
|
||||||
lengthPrefixedData.append(frame)
|
lengthPrefixedData.append(frame)
|
||||||
connection.send(content: lengthPrefixedData, completion: .contentProcessed { _ in
|
connection.send(content: lengthPrefixedData, completion: .contentProcessed { error in
|
||||||
|
if let error = error {
|
||||||
|
log("[NearbyConnection \(self.id)] Error while sending frame: \(error)")
|
||||||
|
}
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
completion()
|
completion()
|
||||||
}
|
}
|
||||||
@@ -357,9 +402,17 @@ class NearbyConnection {
|
|||||||
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
|
let d2dMsg = try Securegcm_DeviceToDeviceMessage(serializedBytes: decryptedData)
|
||||||
|
|
||||||
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
|
guard d2dMsg.hasMessage, d2dMsg.hasSequenceNumber else { throw NearbyError.requiredFieldMissing("d2dMessage.message|sequenceNumber") }
|
||||||
clientSeq += 1
|
let receivedSeq = d2dMsg.sequenceNumber
|
||||||
|
let expectedSeq = clientSeq + 1
|
||||||
|
|
||||||
guard d2dMsg.sequenceNumber == clientSeq else { throw NearbyError.protocolError("Wrong sequence number. Expected \(clientSeq), got \(d2dMsg.sequenceNumber)") }
|
if receivedSeq == expectedSeq {
|
||||||
|
clientSeq = receivedSeq
|
||||||
|
} else if receivedSeq <= clientSeq {
|
||||||
|
log("[NearbyConnection \(self.id)] Ignoring duplicate/stale frame sequence \(receivedSeq). Current sequence is \(clientSeq)")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
throw NearbyError.protocolError("Wrong sequence number. Expected \(expectedSeq), got \(receivedSeq)")
|
||||||
|
}
|
||||||
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
|
let offlineFrame = try Location_Nearby_Connections_OfflineFrame(serializedBytes: d2dMsg.message)
|
||||||
|
|
||||||
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
|
if offlineFrame.hasV1, offlineFrame.v1.hasType, case .payloadTransfer = offlineFrame.v1.type {
|
||||||
@@ -371,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")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,12 +490,18 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
|
else if offlineFrame.hasV1, offlineFrame.v1.hasType, case .keepAlive = offlineFrame.v1.type {
|
||||||
|
if offlineFrame.v1.hasKeepAlive {
|
||||||
let bytesTransferred = self.bytesTransferred
|
let keepAlive = offlineFrame.v1.keepAlive
|
||||||
let gigabytesTransferred = Double(bytesTransferred) / 1_000_000_000
|
if !keepAlive.hasAck || !keepAlive.ack {
|
||||||
|
|
||||||
log("[NearbyConnection \(self.id)] Sent keep-alive, \(self.bytesTransferred) bytes (\(gigabytesTransferred) GB) sent")
|
|
||||||
sendKeepAlive(ack: true)
|
sendKeepAlive(ack: true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Older peers can omit the keepAlive payload; default to ACK reply.
|
||||||
|
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 {
|
||||||
@@ -461,17 +535,12 @@ class NearbyConnection {
|
|||||||
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
|
guard peerKey.hasEcP256PublicKey else { throw NearbyError.requiredFieldMissing("peerKey.ecP256PublicKey") }
|
||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
var clientX = peerKey.ecP256PublicKey.x
|
let clientX = peerKey.ecP256PublicKey.x.ensureLength(length: 32)
|
||||||
var clientY = peerKey.ecP256PublicKey.y
|
let clientY = peerKey.ecP256PublicKey.y.ensureLength(length: 32)
|
||||||
if clientX.count > 32 {
|
|
||||||
clientX = clientX.suffix(32)
|
|
||||||
}
|
|
||||||
if clientY.count > 32 {
|
|
||||||
clientY = clientY.suffix(32)
|
|
||||||
}
|
|
||||||
let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY))))
|
let key = try ECPublicKey(domain: domain, w: Point(BInt(magnitude: [UInt8](clientX)), BInt(magnitude: [UInt8](clientY))))
|
||||||
|
|
||||||
let dhs = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())!
|
let dhsRaw = try (privateKey?.domain.multiplyPoint(key.w, privateKey!.s).x.asMagnitudeBytes())!
|
||||||
|
let dhs = Data(dhsRaw).ensureLength(length: 32)
|
||||||
var sha = SHA256()
|
var sha = SHA256()
|
||||||
sha.update(data: dhs)
|
sha.update(data: dhs)
|
||||||
let derivedSecretKey = Data(sha.finalize())
|
let derivedSecretKey = Data(sha.finalize())
|
||||||
@@ -515,12 +584,27 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
cancelled = true
|
||||||
|
if encryptionDone {
|
||||||
|
var cancel = Sharing_Nearby_Frame()
|
||||||
|
cancel.version = .v1
|
||||||
|
cancel.v1 = Sharing_Nearby_V1Frame()
|
||||||
|
cancel.v1.type = .cancel
|
||||||
|
try? sendTransferSetupFrame(cancel)
|
||||||
|
}
|
||||||
|
try? sendDisconnectionAndDisconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func disconnect() {
|
func disconnect() {
|
||||||
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
|
log("[NearbyConnection \(self.id)] Disconnecting from connection.")
|
||||||
|
|
||||||
connection.stateUpdateHandler = nil
|
connection.stateUpdateHandler = nil
|
||||||
inactivityTimer?.cancel()
|
inactivityTimer?.cancel()
|
||||||
inactivityTimer = nil
|
inactivityTimer = nil
|
||||||
|
dataTransferInactivityTimer?.cancel()
|
||||||
|
dataTransferInactivityTimer = nil
|
||||||
connectionClosed = true
|
connectionClosed = true
|
||||||
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
|
connection.send(content: nil, isComplete: true, completion: .contentProcessed { _ in })
|
||||||
}
|
}
|
||||||
@@ -532,14 +616,22 @@ class NearbyConnection {
|
|||||||
offlineFrame.v1.type = .disconnection
|
offlineFrame.v1.type = .disconnection
|
||||||
offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame()
|
offlineFrame.v1.disconnection = Location_Nearby_Connections_DisconnectionFrame()
|
||||||
|
|
||||||
if encryptionDone {
|
if connectionClosed {
|
||||||
try encryptAndSendOfflineFrame(offlineFrame)
|
disconnect()
|
||||||
} else {
|
return
|
||||||
try sendFrameAsync(offlineFrame.serializedData())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finishDisconnect: () -> Void = {
|
||||||
log("[NearbyConnection \(self.id)] Sent disconnection frame during sendDisconnectionAndDisconnect")
|
log("[NearbyConnection \(self.id)] Sent disconnection frame during sendDisconnectionAndDisconnect")
|
||||||
disconnect()
|
self.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if encryptionDone {
|
||||||
|
try encryptAndSendOfflineFrame(offlineFrame, completion: finishDisconnect)
|
||||||
|
} else {
|
||||||
|
let data = try offlineFrame.serializedData()
|
||||||
|
sendFrameAsync(data, completion: finishDisconnect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -568,25 +660,36 @@ class NearbyConnection {
|
|||||||
} else {
|
} else {
|
||||||
try sendFrameAsync(offlineFrame.serializedData())
|
try sendFrameAsync(offlineFrame.serializedData())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.lastKeepAliveFrameSent = Date()
|
||||||
} catch {
|
} catch {
|
||||||
log("[NearbyConnection \(self.id)] Error sending KEEP_ALIVE: \(error)")
|
log("[NearbyConnection \(self.id)] Error sending KEEP_ALIVE: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log("[NearbyConnection \(self.id)] Sent keep-alive \(ack ? "ack" : "no ack"), \(self.bytesTransferred) bytes (\(Double(bytesTransferred) / 1_000_000_000) GB) sent")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func startAndResetHeartbeatTimer() {
|
func startAndResetHeartbeatTimer() {
|
||||||
|
if connectionClosed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if encryptionDone && lastKeepAliveFrameSent.isOlderThan(seconds: ackInterval) {
|
||||||
|
sendKeepAlive(ack: false)
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel previous timer if any
|
// Cancel previous timer if any
|
||||||
inactivityTimer?.cancel()
|
inactivityTimer?.cancel()
|
||||||
|
|
||||||
inactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
|
inactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
|
||||||
inactivityTimer?.schedule(deadline: .now() + timeoutInterval)
|
inactivityTimer?.schedule(deadline: .now() + ackTimeoutInterval)
|
||||||
inactivityTimer?.setEventHandler { [weak self] in
|
inactivityTimer?.setEventHandler { [weak self] in
|
||||||
|
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if !self.connectionClosed {
|
if !self.connectionClosed {
|
||||||
log("[NearbyConnection \(self.id)] Connection timeout: No message received in \(self.timeoutInterval) seconds")
|
log("[NearbyConnection \(self.id)] Connection timeout: No message received in \(self.ackTimeoutInterval) seconds")
|
||||||
self.lastError = NearbyError.canceled(reason: .timedOut)
|
self.lastError = NearbyError.canceled(reason: .timedOut)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
}
|
}
|
||||||
@@ -597,7 +700,14 @@ class NearbyConnection {
|
|||||||
|
|
||||||
|
|
||||||
func startDataTransferTimer(previousBytesTransferred: Int64) {
|
func startDataTransferTimer(previousBytesTransferred: Int64) {
|
||||||
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + timeoutInterval) {
|
dataTransferInactivityTimer?.cancel()
|
||||||
|
|
||||||
|
dataTransferInactivityTimer = DispatchSource.makeTimerSource(queue: NearbyConnection.dispatchQueue)
|
||||||
|
dataTransferInactivityTimer?.schedule(deadline: .now() + ackTimeoutInterval)
|
||||||
|
dataTransferInactivityTimer?.setEventHandler { [weak self] in
|
||||||
|
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard !self.connectionClosed, self.isTransferring else { return }
|
||||||
|
|
||||||
if previousBytesTransferred < self.bytesTransferred {
|
if previousBytesTransferred < self.bytesTransferred {
|
||||||
// everything good, transferred more than last check, schedule next check
|
// everything good, transferred more than last check, schedule next check
|
||||||
@@ -605,13 +715,13 @@ class NearbyConnection {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// connection stale, need to abort
|
// connection stale, need to abort
|
||||||
if !self.connectionClosed {
|
log("[NearbyConnection \(self.id)] Connection timeout: No more data received in \(self.ackTimeoutInterval) seconds")
|
||||||
log("[NearbyConnection \(self.id)] Connection timeout: No more data received in \(self.timeoutInterval) seconds")
|
|
||||||
self.lastError = NearbyError.canceled(reason: .timedOut)
|
self.lastError = NearbyError.canceled(reason: .timedOut)
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
dataTransferInactivityTimer?.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,18 +14,22 @@ import UniformTypeIdentifiers
|
|||||||
import BigInt
|
import BigInt
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
import ASN1
|
import ASN1
|
||||||
import BigInt // required dependency of ASN1
|
import BigInt
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
|
||||||
class OutboundNearbyConnection: NearbyConnection {
|
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?
|
||||||
@@ -34,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)
|
||||||
|
|
||||||
@@ -62,6 +75,18 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
override func disconnect() {
|
override func disconnect() {
|
||||||
super.disconnect()
|
super.disconnect()
|
||||||
|
|
||||||
|
// delete all files
|
||||||
|
for url in urlsToSend {
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Deleted file at \(url).")
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Failed to delete file at \(url): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let error = lastError {
|
if let error = lastError {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.failedWithError(connection: self, error: error)
|
self.delegate?.failedWithError(connection: self, error: error)
|
||||||
@@ -70,19 +95,6 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func cancel() {
|
|
||||||
cancelled = true
|
|
||||||
if encryptionDone {
|
|
||||||
var cancel = Sharing_Nearby_Frame()
|
|
||||||
cancel.version = .v1
|
|
||||||
cancel.v1 = Sharing_Nearby_V1Frame()
|
|
||||||
cancel.v1.type = .cancel
|
|
||||||
try? sendTransferSetupFrame(cancel)
|
|
||||||
}
|
|
||||||
try? sendDisconnectionAndDisconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override func connectionReady() {
|
override func connectionReady() {
|
||||||
super.connectionReady()
|
super.connectionReady()
|
||||||
do {
|
do {
|
||||||
@@ -147,15 +159,50 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .sentPairedKeyEncryption:
|
case .sentPairedKeyEncryption:
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyEncryption {
|
||||||
try processPairedKeyEncryption(frame: frame)
|
try processPairedKeyEncryption(frame: frame)
|
||||||
case .sentPairedKeyResult:
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Received pairedKeyResult before pairedKeyEncryption; continuing for legacy compatibility.")
|
||||||
try processPairedKeyResult(frame: frame)
|
try processPairedKeyResult(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .progressUpdate || frame.v1.type == .response {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyEncryption.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyEncryption")
|
||||||
|
case .sentPairedKeyResult:
|
||||||
|
if frame.hasV1, frame.v1.hasPairedKeyResult {
|
||||||
|
try processPairedKeyResult(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .progressUpdate || frame.v1.type == .response {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for pairedKeyResult.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.pairedKeyResult")
|
||||||
case .sentIntroduction:
|
case .sentIntroduction:
|
||||||
|
if frame.version == .v1, frame.v1.type == .response {
|
||||||
try processConsent(frame: frame)
|
try processConsent(frame: frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if frame.hasV1, frame.v1.hasType {
|
||||||
|
if frame.v1.type == .pairedKeyEncryption || frame.v1.type == .pairedKeyResult || frame.v1.type == .progressUpdate {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring duplicate/intermediate transfer setup frame \(frame.v1.type) while waiting for consent.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw NearbyError.requiredFieldMissing("sharingNearbyFrame.v1.type==response")
|
||||||
case .sendingFiles:
|
case .sendingFiles:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
assertionFailure("[OutboundNearbyConnection \(self.id)] Unexpected state \(currentState)")
|
log("[OutboundNearbyConnection \(self.id)] Unexpected state \(currentState)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,8 +222,8 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
frame.v1 = Location_Nearby_Connections_V1Frame()
|
frame.v1 = Location_Nearby_Connections_V1Frame()
|
||||||
frame.v1.type = .connectionRequest
|
frame.v1.type = .connectionRequest
|
||||||
frame.v1.connectionRequest = Location_Nearby_Connections_ConnectionRequestFrame()
|
frame.v1.connectionRequest = Location_Nearby_Connections_ConnectionRequestFrame()
|
||||||
frame.v1.connectionRequest.endpointID = String(bytes: NearbyConnectionManager.shared.endpointID, encoding: .ascii)!
|
frame.v1.connectionRequest.endpointID = Data(NearbyConnectionManager.shared.endpointID)
|
||||||
frame.v1.connectionRequest.endpointName = endpointInfo.name ?? "QuickDrop"
|
frame.v1.connectionRequest.endpointName = Data((endpointInfo.name ?? "QuickDrop").utf8)
|
||||||
frame.v1.connectionRequest.endpointInfo = endpointInfo.serialize()
|
frame.v1.connectionRequest.endpointInfo = endpointInfo.serialize()
|
||||||
frame.v1.connectionRequest.mediums = [.wifiLan]
|
frame.v1.connectionRequest.mediums = [.wifiLan]
|
||||||
try sendFrameAsync(frame.serializedData())
|
try sendFrameAsync(frame.serializedData())
|
||||||
@@ -269,6 +316,12 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
|
private func processConnectionResponse(frame: Location_Nearby_Connections_OfflineFrame) throws {
|
||||||
|
|
||||||
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
|
guard frame.version == .v1 else { throw NearbyError.protocolError("Unexpected offline frame version \(frame.version)") }
|
||||||
|
guard frame.hasV1, frame.v1.hasType else { throw NearbyError.requiredFieldMissing("offlineFrame.v1.type") }
|
||||||
|
|
||||||
|
if frame.v1.type == .keepAlive {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Ignoring keep-alive while waiting for connection response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
guard frame.v1.type == .connectionResponse else { throw NearbyError.protocolError("Unexpected frame type \(frame.v1.type)") }
|
||||||
|
|
||||||
@@ -327,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
|
||||||
@@ -336,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()
|
||||||
@@ -347,6 +411,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
meta.textTitle = textToSend
|
meta.textTitle = textToSend
|
||||||
meta.size = Int64(textToSend.utf8.count)
|
meta.size = Int64(textToSend.utf8.count)
|
||||||
meta.payloadID = textPayloadID
|
meta.payloadID = textPayloadID
|
||||||
|
meta.id = Int64.random(in: Int64.min ... Int64.max)
|
||||||
introduction.v1.introduction.textMetadata.append(meta)
|
introduction.v1.introduction.textMetadata.append(meta)
|
||||||
|
|
||||||
} else if hasURL() {
|
} else if hasURL() {
|
||||||
@@ -355,6 +420,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
meta.textTitle = urlsToSend[0].host ?? "URL"
|
meta.textTitle = urlsToSend[0].host ?? "URL"
|
||||||
meta.size = Int64(urlsToSend[0].absoluteString.utf8.count)
|
meta.size = Int64(urlsToSend[0].absoluteString.utf8.count)
|
||||||
meta.payloadID = textPayloadID
|
meta.payloadID = textPayloadID
|
||||||
|
meta.id = Int64.random(in: Int64.min ... Int64.max)
|
||||||
introduction.v1.introduction.textMetadata.append(meta)
|
introduction.v1.introduction.textMetadata.append(meta)
|
||||||
} else {
|
} else {
|
||||||
for url in urlsToSend {
|
for url in urlsToSend {
|
||||||
@@ -382,12 +448,15 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
} else {
|
} else {
|
||||||
meta.type = .unknown
|
meta.type = .unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
meta.payloadID = Int64.random(in: Int64.min ... Int64.max)
|
meta.payloadID = Int64.random(in: Int64.min ... Int64.max)
|
||||||
try queue.append(OutgoingFileTransfer(url: url, payloadID: meta.payloadID, handle: FileHandle(forReadingFrom: url), totalBytes: meta.size, currentOffset: 0))
|
meta.id = Int64.random(in: Int64.min ... Int64.max)
|
||||||
|
|
||||||
|
try queue.append(OutgoingFileTransfer(url: url, fileName: meta.name, payloadID: meta.payloadID, handle: FileHandle(forReadingFrom: url), totalBytes: meta.size, currentOffset: 0))
|
||||||
introduction.v1.introduction.fileMetadata.append(meta)
|
introduction.v1.introduction.fileMetadata.append(meta)
|
||||||
totalBytesToSend += meta.size
|
totalBytesToSend += meta.size
|
||||||
|
|
||||||
log("[OutboundNearbyConnection \(self.id)] Sending file with \(meta.size) bytes")
|
log("[OutboundNearbyConnection \(self.id)] Sending file with \(meta.size) bytes and mime type \(meta.mimeType) and type \(meta.type) and name \(meta.name)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try sendTransferSetupFrame(introduction)
|
try sendTransferSetupFrame(introduction)
|
||||||
@@ -427,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
|
||||||
}
|
}
|
||||||
@@ -454,8 +601,18 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
}
|
}
|
||||||
if queue.isEmpty {
|
if queue.isEmpty {
|
||||||
log("[OutboundNearbyConnection \(self.id)] Disconnecting because all files have been transferred")
|
log("[OutboundNearbyConnection \(self.id)] Disconnecting because all files have been transferred")
|
||||||
try sendDisconnectionAndDisconnect()
|
|
||||||
delegate?.transferFinished(connection: self)
|
// Delay disconnection to ensure EOF frame is processed at peer
|
||||||
|
NearbyConnection.dispatchQueue.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
do {
|
||||||
|
try self.sendDisconnectionAndDisconnect()
|
||||||
|
self.delegate?.transferFinished(connection: self)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
log("[OutboundNearbyConnection \(self.id)] Error while sending disconnection: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentTransfer = queue.removeFirst()
|
currentTransfer = queue.removeFirst()
|
||||||
@@ -476,6 +633,7 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
transfer.payloadHeader.type = .file
|
transfer.payloadHeader.type = .file
|
||||||
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
|
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
|
||||||
transfer.payloadHeader.isSensitive = false
|
transfer.payloadHeader.isSensitive = false
|
||||||
|
transfer.payloadHeader.fileName = currentTransfer!.fileName
|
||||||
currentTransfer!.currentOffset += Int64(fileBuffer.count)
|
currentTransfer!.currentOffset += Int64(fileBuffer.count)
|
||||||
|
|
||||||
var wrapper = Location_Nearby_Connections_OfflineFrame()
|
var wrapper = Location_Nearby_Connections_OfflineFrame()
|
||||||
@@ -498,11 +656,12 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
delegate?.updatedTransferProgress(connection: self, progress: Double(bytesTransferred) / Double(totalBytesToSend))
|
delegate?.updatedTransferProgress(connection: self, progress: Double(bytesTransferred) / Double(totalBytesToSend))
|
||||||
|
|
||||||
if currentTransfer!.currentOffset == currentTransfer!.totalBytes {
|
if currentTransfer!.currentOffset == currentTransfer!.totalBytes {
|
||||||
// Signal end of file (yes, all this for one bit)
|
|
||||||
|
// Signal end of file
|
||||||
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
|
var transfer = Location_Nearby_Connections_PayloadTransferFrame()
|
||||||
transfer.packetType = .data
|
transfer.packetType = .data
|
||||||
transfer.payloadChunk.offset = currentTransfer!.currentOffset
|
transfer.payloadChunk.offset = currentTransfer!.currentOffset
|
||||||
transfer.payloadChunk.flags = 1 // <- this one here
|
transfer.payloadChunk.flags = 1
|
||||||
transfer.payloadHeader.id = currentTransfer!.payloadID
|
transfer.payloadHeader.id = currentTransfer!.payloadID
|
||||||
transfer.payloadHeader.type = .file
|
transfer.payloadHeader.type = .file
|
||||||
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
|
transfer.payloadHeader.totalSize = Int64(currentTransfer!.totalBytes)
|
||||||
@@ -528,9 +687,39 @@ class OutboundNearbyConnection: NearbyConnection {
|
|||||||
|
|
||||||
private struct OutgoingFileTransfer {
|
private struct OutgoingFileTransfer {
|
||||||
let url: URL
|
let url: URL
|
||||||
|
let fileName: String
|
||||||
let payloadID: Int64
|
let payloadID: Int64
|
||||||
let handle: FileHandle?
|
let handle: FileHandle?
|
||||||
let totalBytes: Int64
|
let totalBytes: Int64
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,20 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UniformTypeIdentifiers
|
||||||
import Network
|
import Network
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
import SwiftECC
|
import SwiftECC
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
|
||||||
protocol InboundNearbyConnectionDelegate {
|
protocol InboundNearbyConnectionDelegate {
|
||||||
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection, acceptAutomatically: Bool)
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||||
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo, connection: InboundNearbyConnection)
|
||||||
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
|
||||||
func connectionWasTerminated(connection: InboundNearbyConnection, error: Error?)
|
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
|
||||||
|
func showPlusScreen()
|
||||||
|
func pairingSetupConfirmed(connection: InboundNearbyConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -24,13 +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, acceptAutomatically: Bool)
|
func obtainUserConsent(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func connectionWasTerminated(from device: RemoteDeviceInfo, wasPlainTextTransfer: Bool, error: Error?)
|
func obtainedUserConsentAutomatically(transfer: TransferMetadata, device: RemoteDeviceInfo)
|
||||||
func transferProgress(progress: Double)
|
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: Error?)
|
||||||
|
func transferProgress(connectionID: String, progress: Double)
|
||||||
|
func showPlusScreen()
|
||||||
|
func pairingSetupConfirmed()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InboundAppDelegate {
|
||||||
|
func pairingSetupConfirmed() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -50,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
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -108,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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -118,20 +217,124 @@ 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 {
|
||||||
case file
|
case file
|
||||||
case text
|
case text
|
||||||
case url
|
case url
|
||||||
|
case wifiPassword
|
||||||
|
case notificationSync
|
||||||
|
case clipboardSync
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getSummary() -> String {
|
||||||
|
if let textTitle = textDescription {
|
||||||
|
return textTitle
|
||||||
|
} else if files.count == 1 {
|
||||||
|
return fileTypeSummary(for: files[0])
|
||||||
|
} else {
|
||||||
|
return String.localizedStringWithFormat("NFiles".localized(), files.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fileTypeSummary(for file: FileMetadata) -> String {
|
||||||
|
let mimeType = file.mimeType.lowercased()
|
||||||
|
let ext = URL(fileURLWithPath: file.name).pathExtension.lowercased()
|
||||||
|
let type = UTType(mimeType: mimeType) ?? UTType(filenameExtension: ext)
|
||||||
|
|
||||||
|
if let type, type.conforms(to: .pdf) {
|
||||||
|
return "PDF".localized()
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .image) {
|
||||||
|
return "Image".localized()
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
|
||||||
|
return "Video".localized()
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .audio) {
|
||||||
|
return "Audio".localized()
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .text) {
|
||||||
|
return "Text".localized()
|
||||||
|
}
|
||||||
|
if let type, type.conforms(to: .archive) {
|
||||||
|
return "Archive".localized()
|
||||||
|
}
|
||||||
|
if let type, let description = type.localizedDescription {
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
return "File".localized()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getDescription(deviceName: String) -> String {
|
||||||
|
|
||||||
|
let fileStr = getSummary()
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .file:
|
||||||
|
return "DeviceSendingFiles".localized(with: fileStr, deviceName)
|
||||||
|
|
||||||
|
case .text:
|
||||||
|
return "DeviceSendingText".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .url:
|
||||||
|
return "DeviceSendingUrl".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .wifiPassword:
|
||||||
|
return "DeviceSendingWifiPassword".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .notificationSync:
|
||||||
|
if let textDescription, !textDescription.isEmpty {
|
||||||
|
return textDescription
|
||||||
|
}
|
||||||
|
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
|
|
||||||
|
case .clipboardSync:
|
||||||
|
if let textDescription, !textDescription.isEmpty {
|
||||||
|
return textDescription
|
||||||
|
}
|
||||||
|
return "ClipboardSyncConsentPromptFromDevice".localized(with: deviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getPinCodeMessage() -> String {
|
||||||
|
return String(format: "PinCode".localized(), arguments: [pinCode ?? "?"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PairingUseCase: String, Codable, Hashable {
|
||||||
|
case notificationSync = "notification_sync"
|
||||||
|
case clipboardSync = "clipboard_sync"
|
||||||
|
|
||||||
|
var protoValue: Sharing_Nearby_PairingMetadata.UseCase {
|
||||||
|
switch self {
|
||||||
|
case .notificationSync:
|
||||||
|
return .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
return .clipboardSync
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(protoValue: Sharing_Nearby_PairingMetadata.UseCase) {
|
||||||
|
switch protoValue {
|
||||||
|
case .notificationSync:
|
||||||
|
self = .notificationSync
|
||||||
|
case .clipboardSync:
|
||||||
|
self = .clipboardSync
|
||||||
|
case .unknown:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,16 +351,20 @@ public struct EndpointInfo {
|
|||||||
let deviceType: RemoteDeviceInfo.DeviceType
|
let deviceType: RemoteDeviceInfo.DeviceType
|
||||||
let qrCodeData: Data?
|
let qrCodeData: Data?
|
||||||
|
|
||||||
|
static let maxNameChars = 32
|
||||||
|
|
||||||
init(name: String, deviceType: RemoteDeviceInfo.DeviceType) {
|
init(name: String, deviceType: RemoteDeviceInfo.DeviceType) {
|
||||||
self.name = name
|
self.name = String(decoding: Self.sanitizedString(name), as: UTF8.self)
|
||||||
self.deviceType = deviceType
|
self.deviceType = deviceType
|
||||||
self.qrCodeData = nil
|
self.qrCodeData = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init?(data: Data) {
|
init?(data: Data) {
|
||||||
guard data.count > 17 else { return nil }
|
guard data.count > 17 else {
|
||||||
|
log("[EndpointInfo] Invalid endpoint info data received (too short)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
let hasName = (data[0] & 0x10) == 0
|
let hasName = (data[0] & 0x10) == 0
|
||||||
let deviceNameLength: Int
|
let deviceNameLength: Int
|
||||||
@@ -165,8 +372,14 @@ public struct EndpointInfo {
|
|||||||
|
|
||||||
if hasName {
|
if hasName {
|
||||||
deviceNameLength = Int(data[17])
|
deviceNameLength = Int(data[17])
|
||||||
guard data.count >= deviceNameLength + 18 else { return nil }
|
guard data.count >= deviceNameLength + 18 else {
|
||||||
guard let newDeviceName = String(data: data[18..<(18 + deviceNameLength)], encoding: .utf8) else { return nil }
|
log("[EndpointInfo] Invalid endpoint info data received (name length mismatch)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let newDeviceName = String(data: data[18..<(18 + deviceNameLength)], encoding: .utf8) else {
|
||||||
|
log("[EndpointInfo] Invalid endpoint info data received (name decoding failed)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
deviceName = newDeviceName
|
deviceName = newDeviceName
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -213,16 +426,27 @@ public struct EndpointInfo {
|
|||||||
endpointInfo.append(UInt8.random(in: 0...255))
|
endpointInfo.append(UInt8.random(in: 0...255))
|
||||||
}
|
}
|
||||||
// Device name in UTF-8 prefixed with 1-byte length
|
// Device name in UTF-8 prefixed with 1-byte length
|
||||||
var nameChars = [UInt8]((name ?? "").utf8)
|
let nameChars = Self.sanitizedString(self.name ?? "")
|
||||||
if nameChars.count > 255 {
|
|
||||||
nameChars = [UInt8](nameChars[0 ..< 255])
|
|
||||||
}
|
|
||||||
endpointInfo.append(UInt8(nameChars.count))
|
endpointInfo.append(UInt8(nameChars.count))
|
||||||
for ch in nameChars {
|
for ch in nameChars {
|
||||||
endpointInfo.append(UInt8(ch))
|
endpointInfo.append(UInt8(ch))
|
||||||
}
|
}
|
||||||
return Data(endpointInfo)
|
return Data(endpointInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func sanitizedString(_ string: String) -> [UInt8] {
|
||||||
|
var out: [UInt8] = []
|
||||||
|
out.reserveCapacity(min(Self.maxNameChars, string.utf8.count))
|
||||||
|
|
||||||
|
for scalar in string.unicodeScalars {
|
||||||
|
let bytes = Array(scalar.utf8)
|
||||||
|
if out.count + bytes.count > Self.maxNameChars { break }
|
||||||
|
out.append(contentsOf: bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -233,6 +457,7 @@ public enum NearbyError: Error {
|
|||||||
case packetFilterError
|
case packetFilterError
|
||||||
case firewallError
|
case firewallError
|
||||||
case inputOutput
|
case inputOutput
|
||||||
|
case notificationSyncNotTrusted
|
||||||
case canceled(reason: CancellationReason)
|
case canceled(reason: CancellationReason)
|
||||||
|
|
||||||
public enum CancellationReason {
|
public enum CancellationReason {
|
||||||
|
|||||||
@@ -245,8 +245,8 @@ struct Location_Nearby_Connections_ConnectionRequestFrame: @unchecked Sendable {
|
|||||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
// methods supported on all messages.
|
// methods supported on all messages.
|
||||||
|
|
||||||
var endpointID: String {
|
var endpointID: Data {
|
||||||
get {return _storage._endpointID ?? String()}
|
get {return _storage._endpointID ?? Data()}
|
||||||
set {_uniqueStorage()._endpointID = newValue}
|
set {_uniqueStorage()._endpointID = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `endpointID` has been explicitly set.
|
/// Returns true if `endpointID` has been explicitly set.
|
||||||
@@ -254,8 +254,8 @@ struct Location_Nearby_Connections_ConnectionRequestFrame: @unchecked Sendable {
|
|||||||
/// Clears the value of `endpointID`. Subsequent reads from it will return its default value.
|
/// Clears the value of `endpointID`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearEndpointID() {_uniqueStorage()._endpointID = nil}
|
mutating func clearEndpointID() {_uniqueStorage()._endpointID = nil}
|
||||||
|
|
||||||
var endpointName: String {
|
var endpointName: Data {
|
||||||
get {return _storage._endpointName ?? String()}
|
get {return _storage._endpointName ?? Data()}
|
||||||
set {_uniqueStorage()._endpointName = newValue}
|
set {_uniqueStorage()._endpointName = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `endpointName` has been explicitly set.
|
/// Returns true if `endpointName` has been explicitly set.
|
||||||
@@ -2550,8 +2550,8 @@ extension Location_Nearby_Connections_ConnectionRequestFrame: SwiftProtobuf.Mess
|
|||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}endpoint_id\0\u{3}endpoint_name\0\u{3}handshake_data\0\u{1}nonce\0\u{1}mediums\0\u{3}endpoint_info\0\u{3}medium_metadata\0\u{3}keep_alive_interval_millis\0\u{3}keep_alive_timeout_millis\0\u{3}device_type\0\u{3}device_info\0\u{3}connections_device\0\u{3}presence_device\0\u{3}connection_mode\0\u{3}location_hint\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}endpoint_id\0\u{3}endpoint_name\0\u{3}handshake_data\0\u{1}nonce\0\u{1}mediums\0\u{3}endpoint_info\0\u{3}medium_metadata\0\u{3}keep_alive_interval_millis\0\u{3}keep_alive_timeout_millis\0\u{3}device_type\0\u{3}device_info\0\u{3}connections_device\0\u{3}presence_device\0\u{3}connection_mode\0\u{3}location_hint\0")
|
||||||
|
|
||||||
fileprivate class _StorageClass {
|
fileprivate class _StorageClass {
|
||||||
var _endpointID: String? = nil
|
var _endpointID: Data? = nil
|
||||||
var _endpointName: String? = nil
|
var _endpointName: Data? = nil
|
||||||
var _handshakeData: Data? = nil
|
var _handshakeData: Data? = nil
|
||||||
var _nonce: Int32? = nil
|
var _nonce: Int32? = nil
|
||||||
var _mediums: [Location_Nearby_Connections_ConnectionRequestFrame.Medium] = []
|
var _mediums: [Location_Nearby_Connections_ConnectionRequestFrame.Medium] = []
|
||||||
@@ -2606,8 +2606,8 @@ extension Location_Nearby_Connections_ConnectionRequestFrame: SwiftProtobuf.Mess
|
|||||||
// allocates stack space for every case branch when no optimizations are
|
// allocates stack space for every case branch when no optimizations are
|
||||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
switch fieldNumber {
|
switch fieldNumber {
|
||||||
case 1: try { try decoder.decodeSingularStringField(value: &_storage._endpointID) }()
|
case 1: try { try decoder.decodeSingularBytesField(value: &_storage._endpointID) }()
|
||||||
case 2: try { try decoder.decodeSingularStringField(value: &_storage._endpointName) }()
|
case 2: try { try decoder.decodeSingularBytesField(value: &_storage._endpointName) }()
|
||||||
case 3: try { try decoder.decodeSingularBytesField(value: &_storage._handshakeData) }()
|
case 3: try { try decoder.decodeSingularBytesField(value: &_storage._handshakeData) }()
|
||||||
case 4: try { try decoder.decodeSingularInt32Field(value: &_storage._nonce) }()
|
case 4: try { try decoder.decodeSingularInt32Field(value: &_storage._nonce) }()
|
||||||
case 5: try { try decoder.decodeRepeatedEnumField(value: &_storage._mediums) }()
|
case 5: try { try decoder.decodeRepeatedEnumField(value: &_storage._mediums) }()
|
||||||
@@ -2658,10 +2658,10 @@ extension Location_Nearby_Connections_ConnectionRequestFrame: SwiftProtobuf.Mess
|
|||||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
// https://github.com/apple/swift-protobuf/issues/1182
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
try { if let v = _storage._endpointID {
|
try { if let v = _storage._endpointID {
|
||||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||||
} }()
|
} }()
|
||||||
try { if let v = _storage._endpointName {
|
try { if let v = _storage._endpointName {
|
||||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||||
} }()
|
} }()
|
||||||
try { if let v = _storage._handshakeData {
|
try { if let v = _storage._handshakeData {
|
||||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
||||||
|
|||||||
@@ -334,6 +334,151 @@ struct Sharing_Nearby_WifiCredentialsMetadata: Sendable {
|
|||||||
fileprivate var _id: Int64? = nil
|
fileprivate var _id: Int64? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NEXT_ID=10
|
||||||
|
struct Sharing_Nearby_MirroredNotificationMetadata: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var version: Int32 {
|
||||||
|
get {return _version ?? 1}
|
||||||
|
set {_version = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `version` has been explicitly set.
|
||||||
|
var hasVersion: Bool {return self._version != nil}
|
||||||
|
/// Clears the value of `version`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearVersion() {self._version = nil}
|
||||||
|
|
||||||
|
var notificationKey: String {
|
||||||
|
get {return _notificationKey ?? String()}
|
||||||
|
set {_notificationKey = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `notificationKey` has been explicitly set.
|
||||||
|
var hasNotificationKey: Bool {return self._notificationKey != nil}
|
||||||
|
/// Clears the value of `notificationKey`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearNotificationKey() {self._notificationKey = nil}
|
||||||
|
|
||||||
|
var packageName: String {
|
||||||
|
get {return _packageName ?? String()}
|
||||||
|
set {_packageName = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `packageName` has been explicitly set.
|
||||||
|
var hasPackageName: Bool {return self._packageName != nil}
|
||||||
|
/// Clears the value of `packageName`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPackageName() {self._packageName = nil}
|
||||||
|
|
||||||
|
var appName: String {
|
||||||
|
get {return _appName ?? String()}
|
||||||
|
set {_appName = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `appName` has been explicitly set.
|
||||||
|
var hasAppName: Bool {return self._appName != nil}
|
||||||
|
/// Clears the value of `appName`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearAppName() {self._appName = nil}
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
get {return _title ?? String()}
|
||||||
|
set {_title = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `title` has been explicitly set.
|
||||||
|
var hasTitle: Bool {return self._title != nil}
|
||||||
|
/// Clears the value of `title`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearTitle() {self._title = nil}
|
||||||
|
|
||||||
|
var body: String {
|
||||||
|
get {return _body ?? String()}
|
||||||
|
set {_body = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `body` has been explicitly set.
|
||||||
|
var hasBody: Bool {return self._body != nil}
|
||||||
|
/// Clears the value of `body`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearBody() {self._body = nil}
|
||||||
|
|
||||||
|
var postedAtEpochMs: Int64 {
|
||||||
|
get {return _postedAtEpochMs ?? 0}
|
||||||
|
set {_postedAtEpochMs = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `postedAtEpochMs` has been explicitly set.
|
||||||
|
var hasPostedAtEpochMs: Bool {return self._postedAtEpochMs != nil}
|
||||||
|
/// Clears the value of `postedAtEpochMs`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPostedAtEpochMs() {self._postedAtEpochMs = nil}
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
fileprivate var _version: Int32? = nil
|
||||||
|
fileprivate var _notificationKey: String? = nil
|
||||||
|
fileprivate var _packageName: String? = nil
|
||||||
|
fileprivate var _appName: String? = nil
|
||||||
|
fileprivate var _title: String? = nil
|
||||||
|
fileprivate var _body: String? = nil
|
||||||
|
fileprivate var _postedAtEpochMs: Int64? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// NEXT_ID=5
|
||||||
|
struct Sharing_Nearby_PairingMetadata: Sendable {
|
||||||
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
|
// methods supported on all messages.
|
||||||
|
|
||||||
|
var version: Int32 {
|
||||||
|
get {return _version ?? 1}
|
||||||
|
set {_version = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `version` has been explicitly set.
|
||||||
|
var hasVersion: Bool {return self._version != nil}
|
||||||
|
/// Clears the value of `version`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearVersion() {self._version = nil}
|
||||||
|
|
||||||
|
var useCase: Sharing_Nearby_PairingMetadata.UseCase {
|
||||||
|
get {return _useCase ?? .unknown}
|
||||||
|
set {_useCase = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `useCase` has been explicitly set.
|
||||||
|
var hasUseCase: Bool {return self._useCase != nil}
|
||||||
|
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearUseCase() {self._useCase = nil}
|
||||||
|
|
||||||
|
var setupRequest: Bool {
|
||||||
|
get {return _setupRequest ?? false}
|
||||||
|
set {_setupRequest = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `setupRequest` has been explicitly set.
|
||||||
|
var hasSetupRequest: Bool {return self._setupRequest != nil}
|
||||||
|
/// Clears the value of `setupRequest`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearSetupRequest() {self._setupRequest = nil}
|
||||||
|
|
||||||
|
var setupPinHash: Data {
|
||||||
|
get {return _setupPinHash ?? Data()}
|
||||||
|
set {_setupPinHash = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `setupPinHash` has been explicitly set.
|
||||||
|
var hasSetupPinHash: Bool {return self._setupPinHash != nil}
|
||||||
|
/// Clears the value of `setupPinHash`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearSetupPinHash() {self._setupPinHash = nil}
|
||||||
|
|
||||||
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
|
enum UseCase: Int, SwiftProtobuf.Enum, Swift.CaseIterable {
|
||||||
|
case unknown = 0
|
||||||
|
case notificationSync = 1
|
||||||
|
case clipboardSync = 2
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self = .unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
fileprivate var _version: Int32? = nil
|
||||||
|
fileprivate var _useCase: Sharing_Nearby_PairingMetadata.UseCase? = nil
|
||||||
|
fileprivate var _setupRequest: Bool? = nil
|
||||||
|
fileprivate var _setupPinHash: Data? = nil
|
||||||
|
}
|
||||||
|
|
||||||
/// NEXT_ID=8
|
/// NEXT_ID=8
|
||||||
struct Sharing_Nearby_AppMetadata: Sendable {
|
struct Sharing_Nearby_AppMetadata: Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
@@ -505,76 +650,76 @@ struct Sharing_Nearby_Frame: Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// NEXT_ID=8
|
/// NEXT_ID=8
|
||||||
struct Sharing_Nearby_V1Frame: @unchecked Sendable {
|
struct Sharing_Nearby_V1Frame: Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
// methods supported on all messages.
|
// methods supported on all messages.
|
||||||
|
|
||||||
var type: Sharing_Nearby_V1Frame.FrameType {
|
var type: Sharing_Nearby_V1Frame.FrameType {
|
||||||
get {return _storage._type ?? .unknownFrameType}
|
get {return _type ?? .unknownFrameType}
|
||||||
set {_uniqueStorage()._type = newValue}
|
set {_type = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `type` has been explicitly set.
|
/// Returns true if `type` has been explicitly set.
|
||||||
var hasType: Bool {return _storage._type != nil}
|
var hasType: Bool {return self._type != nil}
|
||||||
/// Clears the value of `type`. Subsequent reads from it will return its default value.
|
/// Clears the value of `type`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearType() {_uniqueStorage()._type = nil}
|
mutating func clearType() {self._type = nil}
|
||||||
|
|
||||||
/// At most one of the following fields will be set.
|
/// At most one of the following fields will be set.
|
||||||
var introduction: Sharing_Nearby_IntroductionFrame {
|
var introduction: Sharing_Nearby_IntroductionFrame {
|
||||||
get {return _storage._introduction ?? Sharing_Nearby_IntroductionFrame()}
|
get {return _introduction ?? Sharing_Nearby_IntroductionFrame()}
|
||||||
set {_uniqueStorage()._introduction = newValue}
|
set {_introduction = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `introduction` has been explicitly set.
|
/// Returns true if `introduction` has been explicitly set.
|
||||||
var hasIntroduction: Bool {return _storage._introduction != nil}
|
var hasIntroduction: Bool {return self._introduction != nil}
|
||||||
/// Clears the value of `introduction`. Subsequent reads from it will return its default value.
|
/// Clears the value of `introduction`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearIntroduction() {_uniqueStorage()._introduction = nil}
|
mutating func clearIntroduction() {self._introduction = nil}
|
||||||
|
|
||||||
var connectionResponse: Sharing_Nearby_ConnectionResponseFrame {
|
var connectionResponse: Sharing_Nearby_ConnectionResponseFrame {
|
||||||
get {return _storage._connectionResponse ?? Sharing_Nearby_ConnectionResponseFrame()}
|
get {return _connectionResponse ?? Sharing_Nearby_ConnectionResponseFrame()}
|
||||||
set {_uniqueStorage()._connectionResponse = newValue}
|
set {_connectionResponse = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `connectionResponse` has been explicitly set.
|
/// Returns true if `connectionResponse` has been explicitly set.
|
||||||
var hasConnectionResponse: Bool {return _storage._connectionResponse != nil}
|
var hasConnectionResponse: Bool {return self._connectionResponse != nil}
|
||||||
/// Clears the value of `connectionResponse`. Subsequent reads from it will return its default value.
|
/// Clears the value of `connectionResponse`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearConnectionResponse() {_uniqueStorage()._connectionResponse = nil}
|
mutating func clearConnectionResponse() {self._connectionResponse = nil}
|
||||||
|
|
||||||
var pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame {
|
var pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame {
|
||||||
get {return _storage._pairedKeyEncryption ?? Sharing_Nearby_PairedKeyEncryptionFrame()}
|
get {return _pairedKeyEncryption ?? Sharing_Nearby_PairedKeyEncryptionFrame()}
|
||||||
set {_uniqueStorage()._pairedKeyEncryption = newValue}
|
set {_pairedKeyEncryption = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `pairedKeyEncryption` has been explicitly set.
|
/// Returns true if `pairedKeyEncryption` has been explicitly set.
|
||||||
var hasPairedKeyEncryption: Bool {return _storage._pairedKeyEncryption != nil}
|
var hasPairedKeyEncryption: Bool {return self._pairedKeyEncryption != nil}
|
||||||
/// Clears the value of `pairedKeyEncryption`. Subsequent reads from it will return its default value.
|
/// Clears the value of `pairedKeyEncryption`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearPairedKeyEncryption() {_uniqueStorage()._pairedKeyEncryption = nil}
|
mutating func clearPairedKeyEncryption() {self._pairedKeyEncryption = nil}
|
||||||
|
|
||||||
var pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame {
|
var pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame {
|
||||||
get {return _storage._pairedKeyResult ?? Sharing_Nearby_PairedKeyResultFrame()}
|
get {return _pairedKeyResult ?? Sharing_Nearby_PairedKeyResultFrame()}
|
||||||
set {_uniqueStorage()._pairedKeyResult = newValue}
|
set {_pairedKeyResult = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `pairedKeyResult` has been explicitly set.
|
/// Returns true if `pairedKeyResult` has been explicitly set.
|
||||||
var hasPairedKeyResult: Bool {return _storage._pairedKeyResult != nil}
|
var hasPairedKeyResult: Bool {return self._pairedKeyResult != nil}
|
||||||
/// Clears the value of `pairedKeyResult`. Subsequent reads from it will return its default value.
|
/// Clears the value of `pairedKeyResult`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearPairedKeyResult() {_uniqueStorage()._pairedKeyResult = nil}
|
mutating func clearPairedKeyResult() {self._pairedKeyResult = nil}
|
||||||
|
|
||||||
/// NOTE: This field was marked as deprecated in the .proto file.
|
/// NOTE: This field was marked as deprecated in the .proto file.
|
||||||
var certificateInfo: Sharing_Nearby_CertificateInfoFrame {
|
var certificateInfo: Sharing_Nearby_CertificateInfoFrame {
|
||||||
get {return _storage._certificateInfo ?? Sharing_Nearby_CertificateInfoFrame()}
|
get {return _certificateInfo ?? Sharing_Nearby_CertificateInfoFrame()}
|
||||||
set {_uniqueStorage()._certificateInfo = newValue}
|
set {_certificateInfo = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `certificateInfo` has been explicitly set.
|
/// Returns true if `certificateInfo` has been explicitly set.
|
||||||
var hasCertificateInfo: Bool {return _storage._certificateInfo != nil}
|
var hasCertificateInfo: Bool {return self._certificateInfo != nil}
|
||||||
/// Clears the value of `certificateInfo`. Subsequent reads from it will return its default value.
|
/// Clears the value of `certificateInfo`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearCertificateInfo() {_uniqueStorage()._certificateInfo = nil}
|
mutating func clearCertificateInfo() {self._certificateInfo = nil}
|
||||||
|
|
||||||
/// NOTE: This field was marked as deprecated in the .proto file.
|
/// NOTE: This field was marked as deprecated in the .proto file.
|
||||||
var progressUpdate: Sharing_Nearby_ProgressUpdateFrame {
|
var progressUpdate: Sharing_Nearby_ProgressUpdateFrame {
|
||||||
get {return _storage._progressUpdate ?? Sharing_Nearby_ProgressUpdateFrame()}
|
get {return _progressUpdate ?? Sharing_Nearby_ProgressUpdateFrame()}
|
||||||
set {_uniqueStorage()._progressUpdate = newValue}
|
set {_progressUpdate = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `progressUpdate` has been explicitly set.
|
/// Returns true if `progressUpdate` has been explicitly set.
|
||||||
var hasProgressUpdate: Bool {return _storage._progressUpdate != nil}
|
var hasProgressUpdate: Bool {return self._progressUpdate != nil}
|
||||||
/// Clears the value of `progressUpdate`. Subsequent reads from it will return its default value.
|
/// Clears the value of `progressUpdate`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearProgressUpdate() {_uniqueStorage()._progressUpdate = nil}
|
mutating func clearProgressUpdate() {self._progressUpdate = nil}
|
||||||
|
|
||||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
@@ -600,56 +745,98 @@ struct Sharing_Nearby_V1Frame: @unchecked Sendable {
|
|||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
fileprivate var _storage = _StorageClass.defaultInstance
|
fileprivate var _type: Sharing_Nearby_V1Frame.FrameType? = nil
|
||||||
|
fileprivate var _introduction: Sharing_Nearby_IntroductionFrame? = nil
|
||||||
|
fileprivate var _connectionResponse: Sharing_Nearby_ConnectionResponseFrame? = nil
|
||||||
|
fileprivate var _pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame? = nil
|
||||||
|
fileprivate var _pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame? = nil
|
||||||
|
fileprivate var _certificateInfo: Sharing_Nearby_CertificateInfoFrame? = nil
|
||||||
|
fileprivate var _progressUpdate: Sharing_Nearby_ProgressUpdateFrame? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An introduction packet sent by the sending side. Contains a list of files
|
/// An introduction packet sent by the sending side. Contains a list of files
|
||||||
/// they'd like to share.
|
/// they'd like to share.
|
||||||
/// NEXT_ID=10
|
/// NEXT_ID=1003
|
||||||
struct Sharing_Nearby_IntroductionFrame: Sendable {
|
struct Sharing_Nearby_IntroductionFrame: @unchecked Sendable {
|
||||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||||
// methods supported on all messages.
|
// methods supported on all messages.
|
||||||
|
|
||||||
var fileMetadata: [Sharing_Nearby_FileMetadata] = []
|
var fileMetadata: [Sharing_Nearby_FileMetadata] {
|
||||||
|
get {return _storage._fileMetadata}
|
||||||
|
set {_uniqueStorage()._fileMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var textMetadata: [Sharing_Nearby_TextMetadata] = []
|
var textMetadata: [Sharing_Nearby_TextMetadata] {
|
||||||
|
get {return _storage._textMetadata}
|
||||||
|
set {_uniqueStorage()._textMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
/// The required app package to open the content. May be null.
|
/// The required app package to open the content. May be null.
|
||||||
var requiredPackage: String {
|
var requiredPackage: String {
|
||||||
get {return _requiredPackage ?? String()}
|
get {return _storage._requiredPackage ?? String()}
|
||||||
set {_requiredPackage = newValue}
|
set {_uniqueStorage()._requiredPackage = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `requiredPackage` has been explicitly set.
|
/// Returns true if `requiredPackage` has been explicitly set.
|
||||||
var hasRequiredPackage: Bool {return self._requiredPackage != nil}
|
var hasRequiredPackage: Bool {return _storage._requiredPackage != nil}
|
||||||
/// Clears the value of `requiredPackage`. Subsequent reads from it will return its default value.
|
/// Clears the value of `requiredPackage`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearRequiredPackage() {self._requiredPackage = nil}
|
mutating func clearRequiredPackage() {_uniqueStorage()._requiredPackage = nil}
|
||||||
|
|
||||||
var wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] = []
|
var wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] {
|
||||||
|
get {return _storage._wifiCredentialsMetadata}
|
||||||
|
set {_uniqueStorage()._wifiCredentialsMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var appMetadata: [Sharing_Nearby_AppMetadata] = []
|
var appMetadata: [Sharing_Nearby_AppMetadata] {
|
||||||
|
get {return _storage._appMetadata}
|
||||||
|
set {_uniqueStorage()._appMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var startTransfer: Bool {
|
var startTransfer: Bool {
|
||||||
get {return _startTransfer ?? false}
|
get {return _storage._startTransfer ?? false}
|
||||||
set {_startTransfer = newValue}
|
set {_uniqueStorage()._startTransfer = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `startTransfer` has been explicitly set.
|
/// Returns true if `startTransfer` has been explicitly set.
|
||||||
var hasStartTransfer: Bool {return self._startTransfer != nil}
|
var hasStartTransfer: Bool {return _storage._startTransfer != nil}
|
||||||
/// Clears the value of `startTransfer`. Subsequent reads from it will return its default value.
|
/// Clears the value of `startTransfer`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearStartTransfer() {self._startTransfer = nil}
|
mutating func clearStartTransfer() {_uniqueStorage()._startTransfer = nil}
|
||||||
|
|
||||||
var streamMetadata: [Sharing_Nearby_StreamMetadata] = []
|
var streamMetadata: [Sharing_Nearby_StreamMetadata] {
|
||||||
|
get {return _storage._streamMetadata}
|
||||||
|
set {_uniqueStorage()._streamMetadata = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
var useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase {
|
var useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase {
|
||||||
get {return _useCase ?? .unknown}
|
get {return _storage._useCase ?? .unknown}
|
||||||
set {_useCase = newValue}
|
set {_uniqueStorage()._useCase = newValue}
|
||||||
}
|
}
|
||||||
/// Returns true if `useCase` has been explicitly set.
|
/// Returns true if `useCase` has been explicitly set.
|
||||||
var hasUseCase: Bool {return self._useCase != nil}
|
var hasUseCase: Bool {return _storage._useCase != nil}
|
||||||
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
/// Clears the value of `useCase`. Subsequent reads from it will return its default value.
|
||||||
mutating func clearUseCase() {self._useCase = nil}
|
mutating func clearUseCase() {_uniqueStorage()._useCase = nil}
|
||||||
|
|
||||||
var previewPayloadIds: [Int64] = []
|
var previewPayloadIds: [Int64] {
|
||||||
|
get {return _storage._previewPayloadIds}
|
||||||
|
set {_uniqueStorage()._previewPayloadIds = newValue}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata {
|
||||||
|
get {return _storage._mirroredNotificationMetadata ?? Sharing_Nearby_MirroredNotificationMetadata()}
|
||||||
|
set {_uniqueStorage()._mirroredNotificationMetadata = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `mirroredNotificationMetadata` has been explicitly set.
|
||||||
|
var hasMirroredNotificationMetadata: Bool {return _storage._mirroredNotificationMetadata != nil}
|
||||||
|
/// Clears the value of `mirroredNotificationMetadata`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearMirroredNotificationMetadata() {_uniqueStorage()._mirroredNotificationMetadata = nil}
|
||||||
|
|
||||||
|
var pairingMetadata: Sharing_Nearby_PairingMetadata {
|
||||||
|
get {return _storage._pairingMetadata ?? Sharing_Nearby_PairingMetadata()}
|
||||||
|
set {_uniqueStorage()._pairingMetadata = newValue}
|
||||||
|
}
|
||||||
|
/// Returns true if `pairingMetadata` has been explicitly set.
|
||||||
|
var hasPairingMetadata: Bool {return _storage._pairingMetadata != nil}
|
||||||
|
/// Clears the value of `pairingMetadata`. Subsequent reads from it will return its default value.
|
||||||
|
mutating func clearPairingMetadata() {_uniqueStorage()._pairingMetadata = nil}
|
||||||
|
|
||||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||||
|
|
||||||
@@ -666,9 +853,7 @@ struct Sharing_Nearby_IntroductionFrame: Sendable {
|
|||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
fileprivate var _requiredPackage: String? = nil
|
fileprivate var _storage = _StorageClass.defaultInstance
|
||||||
fileprivate var _startTransfer: Bool? = nil
|
|
||||||
fileprivate var _useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase? = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A progress update packet sent by the sending side. Contains transfer progress
|
/// A progress update packet sent by the sending side. Contains transfer progress
|
||||||
@@ -1371,6 +1556,123 @@ extension Sharing_Nearby_WifiCredentialsMetadata.SecurityType: SwiftProtobuf._Pr
|
|||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_SECURITY_TYPE\0\u{1}OPEN\0\u{1}WPA_PSK\0\u{1}WEP\0\u{1}SAE\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_SECURITY_TYPE\0\u{1}OPEN\0\u{1}WPA_PSK\0\u{1}WEP\0\u{1}SAE\0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_MirroredNotificationMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".MirroredNotificationMetadata"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}notification_key\0\u{3}package_name\0\u{3}app_name\0\u{1}title\0\u{1}body\0\u{3}posted_at_epoch_ms\0\u{c}\u{8}\u{1}\u{c}\u{9}\u{1}")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularInt32Field(value: &self._version) }()
|
||||||
|
case 2: try { try decoder.decodeSingularStringField(value: &self._notificationKey) }()
|
||||||
|
case 3: try { try decoder.decodeSingularStringField(value: &self._packageName) }()
|
||||||
|
case 4: try { try decoder.decodeSingularStringField(value: &self._appName) }()
|
||||||
|
case 5: try { try decoder.decodeSingularStringField(value: &self._title) }()
|
||||||
|
case 6: try { try decoder.decodeSingularStringField(value: &self._body) }()
|
||||||
|
case 7: try { try decoder.decodeSingularInt64Field(value: &self._postedAtEpochMs) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._version {
|
||||||
|
try visitor.visitSingularInt32Field(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._notificationKey {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._packageName {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._appName {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._title {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 5)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._body {
|
||||||
|
try visitor.visitSingularStringField(value: v, fieldNumber: 6)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._postedAtEpochMs {
|
||||||
|
try visitor.visitSingularInt64Field(value: v, fieldNumber: 7)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_MirroredNotificationMetadata, rhs: Sharing_Nearby_MirroredNotificationMetadata) -> Bool {
|
||||||
|
if lhs._version != rhs._version {return false}
|
||||||
|
if lhs._notificationKey != rhs._notificationKey {return false}
|
||||||
|
if lhs._packageName != rhs._packageName {return false}
|
||||||
|
if lhs._appName != rhs._appName {return false}
|
||||||
|
if lhs._title != rhs._title {return false}
|
||||||
|
if lhs._body != rhs._body {return false}
|
||||||
|
if lhs._postedAtEpochMs != rhs._postedAtEpochMs {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_PairingMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".PairingMetadata"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}version\0\u{3}use_case\0\u{3}setup_request\0\u{3}setup_pin_hash\0")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularInt32Field(value: &self._version) }()
|
||||||
|
case 2: try { try decoder.decodeSingularEnumField(value: &self._useCase) }()
|
||||||
|
case 3: try { try decoder.decodeSingularBoolField(value: &self._setupRequest) }()
|
||||||
|
case 4: try { try decoder.decodeSingularBytesField(value: &self._setupPinHash) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._version {
|
||||||
|
try visitor.visitSingularInt32Field(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._useCase {
|
||||||
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._setupRequest {
|
||||||
|
try visitor.visitSingularBoolField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._setupPinHash {
|
||||||
|
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_PairingMetadata, rhs: Sharing_Nearby_PairingMetadata) -> Bool {
|
||||||
|
if lhs._version != rhs._version {return false}
|
||||||
|
if lhs._useCase != rhs._useCase {return false}
|
||||||
|
if lhs._setupRequest != rhs._setupRequest {return false}
|
||||||
|
if lhs._setupPinHash != rhs._setupPinHash {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_PairingMetadata.UseCase: SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN\0\u{1}NOTIFICATION_SYNC\0\u{1}CLIPBOARD_SYNC\0")
|
||||||
|
}
|
||||||
|
|
||||||
extension Sharing_Nearby_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
extension Sharing_Nearby_AppMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
static let protoMessageName: String = _protobuf_package + ".AppMetadata"
|
static let protoMessageName: String = _protobuf_package + ".AppMetadata"
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_name\0\u{1}size\0\u{3}payload_id\0\u{1}id\0\u{3}file_name\0\u{3}file_size\0\u{3}package_name\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}app_name\0\u{1}size\0\u{3}payload_id\0\u{1}id\0\u{3}file_name\0\u{3}file_size\0\u{3}package_name\0")
|
||||||
@@ -1531,14 +1833,86 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
static let protoMessageName: String = _protobuf_package + ".V1Frame"
|
static let protoMessageName: String = _protobuf_package + ".V1Frame"
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{1}introduction\0\u{3}connection_response\0\u{3}paired_key_encryption\0\u{3}paired_key_result\0\u{3}certificate_info\0\u{3}progress_update\0")
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}type\0\u{1}introduction\0\u{3}connection_response\0\u{3}paired_key_encryption\0\u{3}paired_key_result\0\u{3}certificate_info\0\u{3}progress_update\0")
|
||||||
|
|
||||||
|
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||||
|
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every case branch when no optimizations are
|
||||||
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
|
switch fieldNumber {
|
||||||
|
case 1: try { try decoder.decodeSingularEnumField(value: &self._type) }()
|
||||||
|
case 2: try { try decoder.decodeSingularMessageField(value: &self._introduction) }()
|
||||||
|
case 3: try { try decoder.decodeSingularMessageField(value: &self._connectionResponse) }()
|
||||||
|
case 4: try { try decoder.decodeSingularMessageField(value: &self._pairedKeyEncryption) }()
|
||||||
|
case 5: try { try decoder.decodeSingularMessageField(value: &self._pairedKeyResult) }()
|
||||||
|
case 6: try { try decoder.decodeSingularMessageField(value: &self._certificateInfo) }()
|
||||||
|
case 7: try { try decoder.decodeSingularMessageField(value: &self._progressUpdate) }()
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||||
|
// The use of inline closures is to circumvent an issue where the compiler
|
||||||
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
|
try { if let v = self._type {
|
||||||
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._introduction {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._connectionResponse {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._pairedKeyEncryption {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._pairedKeyResult {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._certificateInfo {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||||
|
} }()
|
||||||
|
try { if let v = self._progressUpdate {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
|
||||||
|
} }()
|
||||||
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Sharing_Nearby_V1Frame, rhs: Sharing_Nearby_V1Frame) -> Bool {
|
||||||
|
if lhs._type != rhs._type {return false}
|
||||||
|
if lhs._introduction != rhs._introduction {return false}
|
||||||
|
if lhs._connectionResponse != rhs._connectionResponse {return false}
|
||||||
|
if lhs._pairedKeyEncryption != rhs._pairedKeyEncryption {return false}
|
||||||
|
if lhs._pairedKeyResult != rhs._pairedKeyResult {return false}
|
||||||
|
if lhs._certificateInfo != rhs._certificateInfo {return false}
|
||||||
|
if lhs._progressUpdate != rhs._progressUpdate {return false}
|
||||||
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_V1Frame.FrameType: SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_FRAME_TYPE\0\u{1}INTRODUCTION\0\u{1}RESPONSE\0\u{1}PAIRED_KEY_ENCRYPTION\0\u{1}PAIRED_KEY_RESULT\0\u{1}CERTIFICATE_INFO\0\u{1}CANCEL\0\u{1}PROGRESS_UPDATE\0")
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||||
|
static let protoMessageName: String = _protobuf_package + ".IntroductionFrame"
|
||||||
|
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0\u{4}`\u{f}mirrored_notification_metadata\0\u{3}pairing_metadata\0")
|
||||||
|
|
||||||
fileprivate class _StorageClass {
|
fileprivate class _StorageClass {
|
||||||
var _type: Sharing_Nearby_V1Frame.FrameType? = nil
|
var _fileMetadata: [Sharing_Nearby_FileMetadata] = []
|
||||||
var _introduction: Sharing_Nearby_IntroductionFrame? = nil
|
var _textMetadata: [Sharing_Nearby_TextMetadata] = []
|
||||||
var _connectionResponse: Sharing_Nearby_ConnectionResponseFrame? = nil
|
var _requiredPackage: String? = nil
|
||||||
var _pairedKeyEncryption: Sharing_Nearby_PairedKeyEncryptionFrame? = nil
|
var _wifiCredentialsMetadata: [Sharing_Nearby_WifiCredentialsMetadata] = []
|
||||||
var _pairedKeyResult: Sharing_Nearby_PairedKeyResultFrame? = nil
|
var _appMetadata: [Sharing_Nearby_AppMetadata] = []
|
||||||
var _certificateInfo: Sharing_Nearby_CertificateInfoFrame? = nil
|
var _startTransfer: Bool? = nil
|
||||||
var _progressUpdate: Sharing_Nearby_ProgressUpdateFrame? = nil
|
var _streamMetadata: [Sharing_Nearby_StreamMetadata] = []
|
||||||
|
var _useCase: Sharing_Nearby_IntroductionFrame.SharingUseCase? = nil
|
||||||
|
var _previewPayloadIds: [Int64] = []
|
||||||
|
var _mirroredNotificationMetadata: Sharing_Nearby_MirroredNotificationMetadata? = nil
|
||||||
|
var _pairingMetadata: Sharing_Nearby_PairingMetadata? = nil
|
||||||
|
|
||||||
// This property is used as the initial default value for new instances of the type.
|
// This property is used as the initial default value for new instances of the type.
|
||||||
// The type itself is protecting the reference to its storage via CoW semantics.
|
// The type itself is protecting the reference to its storage via CoW semantics.
|
||||||
@@ -1549,13 +1923,17 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
init(copying source: _StorageClass) {
|
init(copying source: _StorageClass) {
|
||||||
_type = source._type
|
_fileMetadata = source._fileMetadata
|
||||||
_introduction = source._introduction
|
_textMetadata = source._textMetadata
|
||||||
_connectionResponse = source._connectionResponse
|
_requiredPackage = source._requiredPackage
|
||||||
_pairedKeyEncryption = source._pairedKeyEncryption
|
_wifiCredentialsMetadata = source._wifiCredentialsMetadata
|
||||||
_pairedKeyResult = source._pairedKeyResult
|
_appMetadata = source._appMetadata
|
||||||
_certificateInfo = source._certificateInfo
|
_startTransfer = source._startTransfer
|
||||||
_progressUpdate = source._progressUpdate
|
_streamMetadata = source._streamMetadata
|
||||||
|
_useCase = source._useCase
|
||||||
|
_previewPayloadIds = source._previewPayloadIds
|
||||||
|
_mirroredNotificationMetadata = source._mirroredNotificationMetadata
|
||||||
|
_pairingMetadata = source._pairingMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1574,13 +1952,17 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
// allocates stack space for every case branch when no optimizations are
|
// allocates stack space for every case branch when no optimizations are
|
||||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||||
switch fieldNumber {
|
switch fieldNumber {
|
||||||
case 1: try { try decoder.decodeSingularEnumField(value: &_storage._type) }()
|
case 1: try { try decoder.decodeRepeatedMessageField(value: &_storage._fileMetadata) }()
|
||||||
case 2: try { try decoder.decodeSingularMessageField(value: &_storage._introduction) }()
|
case 2: try { try decoder.decodeRepeatedMessageField(value: &_storage._textMetadata) }()
|
||||||
case 3: try { try decoder.decodeSingularMessageField(value: &_storage._connectionResponse) }()
|
case 3: try { try decoder.decodeSingularStringField(value: &_storage._requiredPackage) }()
|
||||||
case 4: try { try decoder.decodeSingularMessageField(value: &_storage._pairedKeyEncryption) }()
|
case 4: try { try decoder.decodeRepeatedMessageField(value: &_storage._wifiCredentialsMetadata) }()
|
||||||
case 5: try { try decoder.decodeSingularMessageField(value: &_storage._pairedKeyResult) }()
|
case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._appMetadata) }()
|
||||||
case 6: try { try decoder.decodeSingularMessageField(value: &_storage._certificateInfo) }()
|
case 6: try { try decoder.decodeSingularBoolField(value: &_storage._startTransfer) }()
|
||||||
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._progressUpdate) }()
|
case 7: try { try decoder.decodeRepeatedMessageField(value: &_storage._streamMetadata) }()
|
||||||
|
case 8: try { try decoder.decodeSingularEnumField(value: &_storage._useCase) }()
|
||||||
|
case 9: try { try decoder.decodeRepeatedInt64Field(value: &_storage._previewPayloadIds) }()
|
||||||
|
case 1001: try { try decoder.decodeSingularMessageField(value: &_storage._mirroredNotificationMetadata) }()
|
||||||
|
case 1002: try { try decoder.decodeSingularMessageField(value: &_storage._pairingMetadata) }()
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1593,125 +1975,63 @@ extension Sharing_Nearby_V1Frame: SwiftProtobuf.Message, SwiftProtobuf._MessageI
|
|||||||
// allocates stack space for every if/case branch local when no optimizations
|
// allocates stack space for every if/case branch local when no optimizations
|
||||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||||
// https://github.com/apple/swift-protobuf/issues/1182
|
// https://github.com/apple/swift-protobuf/issues/1182
|
||||||
try { if let v = _storage._type {
|
if !_storage._fileMetadata.isEmpty {
|
||||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
|
try visitor.visitRepeatedMessageField(value: _storage._fileMetadata, fieldNumber: 1)
|
||||||
} }()
|
|
||||||
try { if let v = _storage._introduction {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._connectionResponse {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._pairedKeyEncryption {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._pairedKeyResult {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._certificateInfo {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
|
||||||
} }()
|
|
||||||
try { if let v = _storage._progressUpdate {
|
|
||||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 7)
|
|
||||||
} }()
|
|
||||||
}
|
}
|
||||||
try unknownFields.traverse(visitor: &visitor)
|
if !_storage._textMetadata.isEmpty {
|
||||||
|
try visitor.visitRepeatedMessageField(value: _storage._textMetadata, fieldNumber: 2)
|
||||||
}
|
}
|
||||||
|
try { if let v = _storage._requiredPackage {
|
||||||
static func ==(lhs: Sharing_Nearby_V1Frame, rhs: Sharing_Nearby_V1Frame) -> Bool {
|
|
||||||
if lhs._storage !== rhs._storage {
|
|
||||||
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
|
||||||
let _storage = _args.0
|
|
||||||
let rhs_storage = _args.1
|
|
||||||
if _storage._type != rhs_storage._type {return false}
|
|
||||||
if _storage._introduction != rhs_storage._introduction {return false}
|
|
||||||
if _storage._connectionResponse != rhs_storage._connectionResponse {return false}
|
|
||||||
if _storage._pairedKeyEncryption != rhs_storage._pairedKeyEncryption {return false}
|
|
||||||
if _storage._pairedKeyResult != rhs_storage._pairedKeyResult {return false}
|
|
||||||
if _storage._certificateInfo != rhs_storage._certificateInfo {return false}
|
|
||||||
if _storage._progressUpdate != rhs_storage._progressUpdate {return false}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if !storagesAreEqual {return false}
|
|
||||||
}
|
|
||||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Sharing_Nearby_V1Frame.FrameType: SwiftProtobuf._ProtoNameProviding {
|
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{2}\0UNKNOWN_FRAME_TYPE\0\u{1}INTRODUCTION\0\u{1}RESPONSE\0\u{1}PAIRED_KEY_ENCRYPTION\0\u{1}PAIRED_KEY_RESULT\0\u{1}CERTIFICATE_INFO\0\u{1}CANCEL\0\u{1}PROGRESS_UPDATE\0")
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Sharing_Nearby_IntroductionFrame: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
|
||||||
static let protoMessageName: String = _protobuf_package + ".IntroductionFrame"
|
|
||||||
static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{3}file_metadata\0\u{3}text_metadata\0\u{3}required_package\0\u{3}wifi_credentials_metadata\0\u{3}app_metadata\0\u{3}start_transfer\0\u{3}stream_metadata\0\u{3}use_case\0\u{3}preview_payload_ids\0")
|
|
||||||
|
|
||||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
|
||||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
|
||||||
// The use of inline closures is to circumvent an issue where the compiler
|
|
||||||
// allocates stack space for every case branch when no optimizations are
|
|
||||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
|
||||||
switch fieldNumber {
|
|
||||||
case 1: try { try decoder.decodeRepeatedMessageField(value: &self.fileMetadata) }()
|
|
||||||
case 2: try { try decoder.decodeRepeatedMessageField(value: &self.textMetadata) }()
|
|
||||||
case 3: try { try decoder.decodeSingularStringField(value: &self._requiredPackage) }()
|
|
||||||
case 4: try { try decoder.decodeRepeatedMessageField(value: &self.wifiCredentialsMetadata) }()
|
|
||||||
case 5: try { try decoder.decodeRepeatedMessageField(value: &self.appMetadata) }()
|
|
||||||
case 6: try { try decoder.decodeSingularBoolField(value: &self._startTransfer) }()
|
|
||||||
case 7: try { try decoder.decodeRepeatedMessageField(value: &self.streamMetadata) }()
|
|
||||||
case 8: try { try decoder.decodeSingularEnumField(value: &self._useCase) }()
|
|
||||||
case 9: try { try decoder.decodeRepeatedInt64Field(value: &self.previewPayloadIds) }()
|
|
||||||
default: break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
|
||||||
// The use of inline closures is to circumvent an issue where the compiler
|
|
||||||
// allocates stack space for every if/case branch local when no optimizations
|
|
||||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
|
||||||
// https://github.com/apple/swift-protobuf/issues/1182
|
|
||||||
if !self.fileMetadata.isEmpty {
|
|
||||||
try visitor.visitRepeatedMessageField(value: self.fileMetadata, fieldNumber: 1)
|
|
||||||
}
|
|
||||||
if !self.textMetadata.isEmpty {
|
|
||||||
try visitor.visitRepeatedMessageField(value: self.textMetadata, fieldNumber: 2)
|
|
||||||
}
|
|
||||||
try { if let v = self._requiredPackage {
|
|
||||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||||
} }()
|
} }()
|
||||||
if !self.wifiCredentialsMetadata.isEmpty {
|
if !_storage._wifiCredentialsMetadata.isEmpty {
|
||||||
try visitor.visitRepeatedMessageField(value: self.wifiCredentialsMetadata, fieldNumber: 4)
|
try visitor.visitRepeatedMessageField(value: _storage._wifiCredentialsMetadata, fieldNumber: 4)
|
||||||
}
|
}
|
||||||
if !self.appMetadata.isEmpty {
|
if !_storage._appMetadata.isEmpty {
|
||||||
try visitor.visitRepeatedMessageField(value: self.appMetadata, fieldNumber: 5)
|
try visitor.visitRepeatedMessageField(value: _storage._appMetadata, fieldNumber: 5)
|
||||||
}
|
}
|
||||||
try { if let v = self._startTransfer {
|
try { if let v = _storage._startTransfer {
|
||||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
|
||||||
} }()
|
} }()
|
||||||
if !self.streamMetadata.isEmpty {
|
if !_storage._streamMetadata.isEmpty {
|
||||||
try visitor.visitRepeatedMessageField(value: self.streamMetadata, fieldNumber: 7)
|
try visitor.visitRepeatedMessageField(value: _storage._streamMetadata, fieldNumber: 7)
|
||||||
}
|
}
|
||||||
try { if let v = self._useCase {
|
try { if let v = _storage._useCase {
|
||||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
|
try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
|
||||||
} }()
|
} }()
|
||||||
if !self.previewPayloadIds.isEmpty {
|
if !_storage._previewPayloadIds.isEmpty {
|
||||||
try visitor.visitRepeatedInt64Field(value: self.previewPayloadIds, fieldNumber: 9)
|
try visitor.visitRepeatedInt64Field(value: _storage._previewPayloadIds, fieldNumber: 9)
|
||||||
|
}
|
||||||
|
try { if let v = _storage._mirroredNotificationMetadata {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 1001)
|
||||||
|
} }()
|
||||||
|
try { if let v = _storage._pairingMetadata {
|
||||||
|
try visitor.visitSingularMessageField(value: v, fieldNumber: 1002)
|
||||||
|
} }()
|
||||||
}
|
}
|
||||||
try unknownFields.traverse(visitor: &visitor)
|
try unknownFields.traverse(visitor: &visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: Sharing_Nearby_IntroductionFrame, rhs: Sharing_Nearby_IntroductionFrame) -> Bool {
|
static func ==(lhs: Sharing_Nearby_IntroductionFrame, rhs: Sharing_Nearby_IntroductionFrame) -> Bool {
|
||||||
if lhs.fileMetadata != rhs.fileMetadata {return false}
|
if lhs._storage !== rhs._storage {
|
||||||
if lhs.textMetadata != rhs.textMetadata {return false}
|
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
||||||
if lhs._requiredPackage != rhs._requiredPackage {return false}
|
let _storage = _args.0
|
||||||
if lhs.wifiCredentialsMetadata != rhs.wifiCredentialsMetadata {return false}
|
let rhs_storage = _args.1
|
||||||
if lhs.appMetadata != rhs.appMetadata {return false}
|
if _storage._fileMetadata != rhs_storage._fileMetadata {return false}
|
||||||
if lhs._startTransfer != rhs._startTransfer {return false}
|
if _storage._textMetadata != rhs_storage._textMetadata {return false}
|
||||||
if lhs.streamMetadata != rhs.streamMetadata {return false}
|
if _storage._requiredPackage != rhs_storage._requiredPackage {return false}
|
||||||
if lhs._useCase != rhs._useCase {return false}
|
if _storage._wifiCredentialsMetadata != rhs_storage._wifiCredentialsMetadata {return false}
|
||||||
if lhs.previewPayloadIds != rhs.previewPayloadIds {return false}
|
if _storage._appMetadata != rhs_storage._appMetadata {return false}
|
||||||
|
if _storage._startTransfer != rhs_storage._startTransfer {return false}
|
||||||
|
if _storage._streamMetadata != rhs_storage._streamMetadata {return false}
|
||||||
|
if _storage._useCase != rhs_storage._useCase {return false}
|
||||||
|
if _storage._previewPayloadIds != rhs_storage._previewPayloadIds {return false}
|
||||||
|
if _storage._mirroredNotificationMetadata != rhs_storage._mirroredNotificationMetadata {return false}
|
||||||
|
if _storage._pairingMetadata != rhs_storage._pairingMetadata {return false}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !storagesAreEqual {return false}
|
||||||
|
}
|
||||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ message ConnectionRequestFrame {
|
|||||||
}
|
}
|
||||||
// LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto)
|
// LINT.ThenChange(//depot/google3/third_party/nearby/proto/connections_enums.proto)
|
||||||
|
|
||||||
optional string endpoint_id = 1;
|
optional bytes endpoint_id = 1;
|
||||||
optional string endpoint_name = 2;
|
optional bytes endpoint_name = 2;
|
||||||
optional bytes handshake_data = 3;
|
optional bytes handshake_data = 3;
|
||||||
// A random number generated for each outgoing connection that is presently
|
// A random number generated for each outgoing connection that is presently
|
||||||
// used to act as a tiebreaker when 2 devices connect to each other
|
// used to act as a tiebreaker when 2 devices connect to each other
|
||||||
|
|||||||
@@ -127,6 +127,32 @@ message WifiCredentialsMetadata {
|
|||||||
optional int64 id = 5;
|
optional int64 id = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEXT_ID=10
|
||||||
|
message MirroredNotificationMetadata {
|
||||||
|
optional int32 version = 1 [default = 1];
|
||||||
|
optional string notification_key = 2;
|
||||||
|
optional string package_name = 3;
|
||||||
|
optional string app_name = 4;
|
||||||
|
optional string title = 5;
|
||||||
|
optional string body = 6;
|
||||||
|
optional int64 posted_at_epoch_ms = 7;
|
||||||
|
reserved 8, 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEXT_ID=5
|
||||||
|
message PairingMetadata {
|
||||||
|
enum UseCase {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
NOTIFICATION_SYNC = 1;
|
||||||
|
CLIPBOARD_SYNC = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional int32 version = 1 [default = 1];
|
||||||
|
optional UseCase use_case = 2 [default = UNKNOWN];
|
||||||
|
optional bool setup_request = 3;
|
||||||
|
optional bytes setup_pin_hash = 4;
|
||||||
|
}
|
||||||
|
|
||||||
// NEXT_ID=8
|
// NEXT_ID=8
|
||||||
message AppMetadata {
|
message AppMetadata {
|
||||||
// The app name. This will be sent in introduction.
|
// The app name. This will be sent in introduction.
|
||||||
@@ -212,7 +238,7 @@ message V1Frame {
|
|||||||
|
|
||||||
// An introduction packet sent by the sending side. Contains a list of files
|
// An introduction packet sent by the sending side. Contains a list of files
|
||||||
// they'd like to share.
|
// they'd like to share.
|
||||||
// NEXT_ID=10
|
// NEXT_ID=1003
|
||||||
message IntroductionFrame {
|
message IntroductionFrame {
|
||||||
enum SharingUseCase {
|
enum SharingUseCase {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
@@ -230,6 +256,8 @@ message IntroductionFrame {
|
|||||||
repeated StreamMetadata stream_metadata = 7;
|
repeated StreamMetadata stream_metadata = 7;
|
||||||
optional SharingUseCase use_case = 8;
|
optional SharingUseCase use_case = 8;
|
||||||
repeated int64 preview_payload_ids = 9;
|
repeated int64 preview_payload_ids = 9;
|
||||||
|
optional MirroredNotificationMetadata mirrored_notification_metadata = 1001;
|
||||||
|
optional PairingMetadata pairing_metadata = 1002;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A progress update packet sent by the sending side. Contains transfer progress
|
// A progress update packet sent by the sending side. Contains transfer progress
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
/// Contains information about the app group used to share data between main app and widget
|
/// Contains information about the app group used to share data between main app and extensions
|
||||||
struct AppGroup {
|
struct AppGroup {
|
||||||
|
|
||||||
/// Identifier of the app group
|
/// Identifier of the app group
|
||||||
@@ -19,3 +20,90 @@ struct AppGroup {
|
|||||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct KeychainStore {
|
||||||
|
private static let service = "com.leonboettger.quickdrop.securestorage"
|
||||||
|
// Must match the shared keychain access group declared in all Apple entitlements.
|
||||||
|
private static let sharedAccessGroup = "92SDAWLN76.com.leonboettger.neardrop"
|
||||||
|
|
||||||
|
static func loadData(account: String) throws -> Data? {
|
||||||
|
var query = baseQuery(account: account)
|
||||||
|
query[kSecReturnData as String] = true
|
||||||
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||||
|
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case errSecSuccess:
|
||||||
|
return result as? Data
|
||||||
|
case errSecItemNotFound:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
throw KeychainStoreError.unexpectedStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func saveData(_ data: Data, account: String) throws {
|
||||||
|
var addQuery = baseQuery(account: account)
|
||||||
|
addQuery[kSecValueData as String] = data
|
||||||
|
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||||
|
|
||||||
|
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
||||||
|
if addStatus == errSecSuccess {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if addStatus != errSecDuplicateItem {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(addStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
let attributesToUpdate: [String: Any] = [
|
||||||
|
kSecValueData as String: data,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
|
||||||
|
]
|
||||||
|
|
||||||
|
let updateStatus = SecItemUpdate(baseQuery(account: account) as CFDictionary, attributesToUpdate as CFDictionary)
|
||||||
|
guard updateStatus == errSecSuccess else {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(updateStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func deleteItem(account: String) throws {
|
||||||
|
let status = SecItemDelete(baseQuery(account: account) as CFDictionary)
|
||||||
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||||
|
throw KeychainStoreError.unexpectedStatus(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static func baseQuery(account: String) -> [String: Any] {
|
||||||
|
var query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecUseDataProtectionKeychain as String: true,
|
||||||
|
]
|
||||||
|
|
||||||
|
query[kSecAttrAccessGroup as String] = sharedAccessGroup
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum KeychainStoreError: LocalizedError {
|
||||||
|
case unexpectedStatus(OSStatus)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case let .unexpectedStatus(status):
|
||||||
|
if let message = SecCopyErrorMessageString(status, nil) as String? {
|
||||||
|
return "Keychain error \(status): \(message)"
|
||||||
|
}
|
||||||
|
return "Keychain error \(status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//
|
||||||
|
// FileManager+DocumentsOpener.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 10.11.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
extension FileManager {
|
||||||
|
public func openDocumentFolder() {
|
||||||
|
#if !EXTENSION
|
||||||
|
let documentsUrl = self.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||||
|
|
||||||
|
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
||||||
|
UIApplication.shared.open(sharedUrl, options: [:])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
// Created by Leon Böttger on 04.04.25.
|
// Created by Leon Böttger on 04.04.25.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import LUI
|
||||||
|
|
||||||
extension Data {
|
extension Data {
|
||||||
func urlSafeBase64EncodedString() -> String {
|
func urlSafeBase64EncodedString() -> String {
|
||||||
@@ -30,17 +31,6 @@ extension Data {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func suffixOfAtMost(numBytes: Int) -> Data {
|
|
||||||
|
|
||||||
if count <= numBytes {
|
|
||||||
return self
|
|
||||||
}
|
|
||||||
|
|
||||||
return subdata(in: count-numBytes..<count)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
static func dataFromUrlSafeBase64(_ str: String) -> Data? {
|
||||||
var regularB64 = String(str.map {
|
var regularB64 = String(str.map {
|
||||||
if $0 == "_" {
|
if $0 == "_" {
|
||||||
@@ -110,35 +100,15 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public enum UserDefaultsKeys: String, CaseIterable {
|
public func fullVersion() -> Bool {
|
||||||
case isEligibleForIap = "isEligibleForIap"
|
#if GITHUB
|
||||||
case appLaunchedBefore = "ShowedWelcomeScreen"
|
|
||||||
case plusVersion = "isPlusVersion"
|
|
||||||
case transmissionCount = "reviewRequestCountKey"
|
|
||||||
case automaticallyAcceptFiles = "automaticallyAcceptFiles"
|
|
||||||
case saveFolderBookmark = "saveFolderBookmark"
|
|
||||||
case openFinderAfterReceiving = "openFinderAfterReceiving"
|
|
||||||
case endpointID = "endpointID"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func isPlusVersion() -> Bool {
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
// Enable full functionality if app distributed directly
|
|
||||||
return DistributionDetector.isDirectDistributionEnabled || UserDefaults.standard.bool(forKey: UserDefaultsKeys.plusVersion.rawValue)
|
|
||||||
#else
|
|
||||||
// iOS
|
|
||||||
return true
|
return true
|
||||||
|
#else
|
||||||
|
return IAPManager.sharedInstance.plusVersionState
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public func isFileTransferRestricted() -> Bool {
|
public func isFileTransferRestricted() -> Bool {
|
||||||
(!isPlusVersion() && incomingTransmissionCount() > 1)
|
(!fullVersion() && Settings.sharedInstance.incomingTransmissionCount > 1)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func incomingTransmissionCount() -> Int {
|
|
||||||
return UserDefaults.standard.integer(forKey: UserDefaultsKeys.transmissionCount.rawValue)
|
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import Foundation
|
|||||||
import SwiftECC
|
import SwiftECC
|
||||||
import Security
|
import Security
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
import LUI
|
||||||
|
|
||||||
class IdentityManager {
|
class IdentityManager {
|
||||||
|
|
||||||
@@ -51,16 +52,20 @@ class IdentityManager {
|
|||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
let (pubKey, privKey) = domain.makeKeyPair()
|
let (pubKey, privKey) = domain.makeKeyPair()
|
||||||
|
|
||||||
AppGroup.appGroupUD.set(Data(privKey.s.asSignedBytes()), forKey: signingKeyTag)
|
let privateKeyData = Data(privKey.s.asSignedBytes())
|
||||||
|
do {
|
||||||
|
try KeychainStore.saveData(privateKeyData, account: signingKeyTag)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to store private key in keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
return (privKey, pubKey)
|
return (privKey, pubKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
private func loadKeys() -> (ECPrivateKey, ECPublicKey)? {
|
||||||
let keyData = AppGroup.appGroupUD.data(forKey: signingKeyTag)
|
guard let keyData = loadStoredKeyData() else { return nil }
|
||||||
|
|
||||||
guard let keyData = keyData else { return nil }
|
|
||||||
|
|
||||||
let domain = Domain.instance(curve: .EC256r1)
|
let domain = Domain.instance(curve: .EC256r1)
|
||||||
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
guard let privateKey = try? ECPrivateKey(domain: domain, s: .init(signed: Array(keyData))) else {
|
||||||
@@ -79,6 +84,32 @@ class IdentityManager {
|
|||||||
|
|
||||||
return (privateKey, publicKey)
|
return (privateKey, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func loadStoredKeyData() -> Data? {
|
||||||
|
do {
|
||||||
|
if let keyData = try KeychainStore.loadData(account: signingKeyTag) {
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
return keyData
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to load private key from keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let legacyKeyData = AppGroup.appGroupUD.data(forKey: signingKeyTag) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try KeychainStore.saveData(legacyKeyData, account: signingKeyTag)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: signingKeyTag)
|
||||||
|
log("[IdentityManager] Migrated private key from shared defaults to keychain.")
|
||||||
|
} catch {
|
||||||
|
log("[IdentityManager] Failed to migrate private key to keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyKeyData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// Localized.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 10.03.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
public extension String {
|
|
||||||
func localized() -> String {
|
|
||||||
let localizedString = NSLocalizedString(self, comment: "")
|
|
||||||
return localizedString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
//
|
|
||||||
// Logger.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 26.02.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import os
|
|
||||||
|
|
||||||
fileprivate let loggerQueue = DispatchQueue(label: "loggerQueue", qos: .utility)
|
|
||||||
|
|
||||||
/// Manages logging functionality
|
|
||||||
final class LogManager {
|
|
||||||
|
|
||||||
/// The URL of the log file
|
|
||||||
let logFileURL: URL?
|
|
||||||
|
|
||||||
/// Shared instance
|
|
||||||
public static let sharedInstance = LogManager()
|
|
||||||
|
|
||||||
@Published var loggingEnabled = true
|
|
||||||
|
|
||||||
|
|
||||||
/// Private initializer
|
|
||||||
private init() {
|
|
||||||
// Use the App Group container
|
|
||||||
let directory = AppGroup.appGroupDirectory
|
|
||||||
|
|
||||||
// Create file name from date
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "dd-MM-yyyy"
|
|
||||||
let dateString = formatter.string(from: Date())
|
|
||||||
let fileName = "\(dateString).log"
|
|
||||||
|
|
||||||
// Full path in app group container
|
|
||||||
self.logFileURL = directory?.appendingPathComponent(fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Writes a specified string to the log file.
|
|
||||||
func writeToFile(string: String) {
|
|
||||||
guard let logFileURL = logFileURL else { return }
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "HH:mm:ss"
|
|
||||||
let timestamp = formatter.string(from: Date())
|
|
||||||
guard let data = (timestamp + " - " + string + "\n").data(using: .utf8) else { return }
|
|
||||||
|
|
||||||
loggerQueue.async {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
if !fileManager.fileExists(atPath: logFileURL.path) {
|
|
||||||
// Clean up older logs
|
|
||||||
self.deleteLogs()
|
|
||||||
// Create file atomically
|
|
||||||
try? data.write(to: logFileURL, options: .atomicWrite)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic append with file locking using FileHandle
|
|
||||||
do {
|
|
||||||
let fileHandle = try FileHandle(forWritingTo: logFileURL)
|
|
||||||
defer { try? fileHandle.close() }
|
|
||||||
|
|
||||||
// Locking is handled by the serial dispatch queue
|
|
||||||
try fileHandle.seekToEnd()
|
|
||||||
try fileHandle.write(contentsOf: data)
|
|
||||||
} catch {
|
|
||||||
print("[Logger] Failed to write to log file: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Gets the contents of the log file.
|
|
||||||
func getLogString() -> String {
|
|
||||||
guard let logFileURL = logFileURL else { return "" }
|
|
||||||
do {
|
|
||||||
return try String(contentsOf: logFileURL, encoding: .utf8)
|
|
||||||
} catch {
|
|
||||||
log("[LUI] Error reading file: \(error.localizedDescription)")
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Deletes all log files in the shared container.
|
|
||||||
func deleteLogs() {
|
|
||||||
log("[LUI] Deleting all logs...")
|
|
||||||
|
|
||||||
guard let directory = AppGroup.appGroupDirectory else { return }
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
guard let allFiles = try? fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let logFiles = allFiles.filter { $0.pathExtension == "log" }
|
|
||||||
for file in logFiles {
|
|
||||||
do {
|
|
||||||
log("[LUI] Removing \(file.lastPathComponent)")
|
|
||||||
try fileManager.removeItem(at: file)
|
|
||||||
} catch {
|
|
||||||
print("[Logger] Failed to delete file: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Logs a specified string
|
|
||||||
public func log(_ text: String) {
|
|
||||||
let logger = LogManager.sharedInstance
|
|
||||||
|
|
||||||
if logger.loggingEnabled {
|
|
||||||
loggerQueue.async {
|
|
||||||
print(text)
|
|
||||||
#if !os(tvOS)
|
|
||||||
let isAppExtension = Bundle.main.bundlePath.hasSuffix(".appex")
|
|
||||||
let prefix = isAppExtension ? "[EXT]" : "[APP]"
|
|
||||||
logger.writeToFile(string: "\(prefix) \(text)")
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//
|
||||||
|
// PairingToken.swift
|
||||||
|
// QuickDrop
|
||||||
|
//
|
||||||
|
// Created by Leon Böttger on 2026-03-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import QRCode
|
||||||
|
import SwiftUI
|
||||||
|
import LUI
|
||||||
|
|
||||||
|
enum PairingToken {
|
||||||
|
static func generate() -> String {
|
||||||
|
Data.randomData(length: 16).urlSafeBase64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func receiverFingerprintHex() -> String? {
|
||||||
|
guard let keyId = IdentityManager.shared.getPublicKey()?.toGenericPublicKey().id() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return keyId.hex.lowercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase) -> String {
|
||||||
|
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func makeQrImage(
|
||||||
|
token: String,
|
||||||
|
receiverFingerprint: String,
|
||||||
|
useCase: PairingUseCase,
|
||||||
|
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
|
||||||
|
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
|
||||||
|
) -> Image? {
|
||||||
|
let payload = qrPayload(token: token, receiverFingerprint: receiverFingerprint, useCase: useCase)
|
||||||
|
do {
|
||||||
|
let qrCodeImage = try QRCode.build
|
||||||
|
.text(payload)
|
||||||
|
.foregroundColor(foregroundColor)
|
||||||
|
.backgroundColor(backgroundColor)
|
||||||
|
.quietZonePixelCount(3)
|
||||||
|
.onPixels.shape(.circle())
|
||||||
|
.eye.shape(.squircle())
|
||||||
|
.errorCorrection(.low)
|
||||||
|
.generate
|
||||||
|
.image(dimension: 1000)
|
||||||
|
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
|
||||||
|
} catch {
|
||||||
|
log("[PairingToken] QR code generation failed: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
//
|
|
||||||
// SaveFilesManager.swift
|
|
||||||
// QuickDrop
|
|
||||||
//
|
|
||||||
// Created by Leon Böttger on 26.07.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import ImageIO
|
|
||||||
#if os(macOS)
|
|
||||||
import AppKit
|
|
||||||
#else
|
|
||||||
import LUI
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public class SaveFilesManager {
|
|
||||||
|
|
||||||
private init() {
|
|
||||||
// remove old temp directory
|
|
||||||
|
|
||||||
let tempPath = FileManager.default.temporaryDirectory
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
|
|
||||||
do {
|
|
||||||
let contents = try fileManager.contentsOfDirectory(at: tempPath, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
var didSomething = false
|
|
||||||
|
|
||||||
for item in contents {
|
|
||||||
didSomething = true
|
|
||||||
try fileManager.removeItem(at: item)
|
|
||||||
}
|
|
||||||
|
|
||||||
if didSomething {
|
|
||||||
log("[SaveFilesManager] Temporary directory cleared.")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to list contents of temp directory: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static let shared = SaveFilesManager()
|
|
||||||
|
|
||||||
let tempDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("Pending")
|
|
||||||
private var securityScopeUrl: URL?
|
|
||||||
|
|
||||||
private var filesFinishedDownloading = [URL]()
|
|
||||||
private var filesFinishedDownloadingSinceLastRun = [URL]()
|
|
||||||
|
|
||||||
public func registerFileFinishedDownloading(_ fileURL: URL) {
|
|
||||||
filesFinishedDownloading.append(fileURL)
|
|
||||||
filesFinishedDownloadingSinceLastRun.append(fileURL)
|
|
||||||
|
|
||||||
// read out EXIF timestamp and manually set file creation date to it
|
|
||||||
applyEXIFTimestamps(at: fileURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func movePendingFilesToTarget() {
|
|
||||||
|
|
||||||
if isPlusVersion() {
|
|
||||||
do {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let files = try fileManager.contentsOfDirectory(at: tempDirectory, includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
let target = getSaveDirectory()
|
|
||||||
var loggedExecution = false
|
|
||||||
|
|
||||||
for file in files {
|
|
||||||
|
|
||||||
if !loggedExecution {
|
|
||||||
log("[SaveFilesManager] Moving pending files to target directory")
|
|
||||||
loggedExecution = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileName = file.lastPathComponent
|
|
||||||
let destinationURL = target.appendingPathComponent(fileName)
|
|
||||||
|
|
||||||
if !filesFinishedDownloading.contains(destinationURL) {
|
|
||||||
log("[SaveFilesManager] File \(file) not finished downloading, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Moving file: \(file.lastPathComponent) to \(destinationURL.lastPathComponent)")
|
|
||||||
|
|
||||||
do {
|
|
||||||
try fileManager.copyItem(at: file, to: destinationURL)
|
|
||||||
|
|
||||||
let progress = Progress()
|
|
||||||
progress.fileURL = destinationURL
|
|
||||||
progress.totalUnitCount = 10
|
|
||||||
progress.kind = .file
|
|
||||||
progress.isPausable = false
|
|
||||||
#if os(macOS)
|
|
||||||
progress.publish()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
progress.completedUnitCount = 10
|
|
||||||
#if os(macOS)
|
|
||||||
progress.unpublish()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
try fileManager.removeItem(at: file)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
log("[SaveFilesManager] Error moving file: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("[SaveFilesManager] Moved all pending files to target directory")
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
// Pending directory doesn't exist or is empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filesFinishedDownloadingSinceLastRun.isEmpty && !isFileTransferRestricted() {
|
|
||||||
|
|
||||||
#if os(macOS)
|
|
||||||
if UserDefaults.standard.bool(forKey: UserDefaultsKeys.openFinderAfterReceiving.rawValue) {
|
|
||||||
log("[SaveFilesManager] Opening \(filesFinishedDownloadingSinceLastRun.count) file(s) in Finder.")
|
|
||||||
NSWorkspace.shared.activateFileViewerSelecting(filesFinishedDownloadingSinceLastRun)
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
openDownloadedFilesFolder()
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Clear the list of finished files
|
|
||||||
filesFinishedDownloadingSinceLastRun.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
stopAccessingSecurityScopedResource()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if !os(macOS)
|
|
||||||
public func openDownloadedFilesFolder() {
|
|
||||||
#if !EXTENSION
|
|
||||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
||||||
|
|
||||||
if let documentsUrl = documentsUrl, let sharedUrl = URL(string: "shareddocuments://\(documentsUrl.path)"), UIApplication.shared.canOpenURL(sharedUrl) {
|
|
||||||
UIApplication.shared.open(sharedUrl, options: [:])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
showAlert(title: "CouldNotOpenDocumentsFolder", message: "CouldNotOpenDocumentsFolderDescription")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
public func stopAccessingSecurityScopedResource() {
|
|
||||||
guard let url = securityScopeUrl else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log("[SaveFilesManager] Stopping access to security scoped resource: \(url)")
|
|
||||||
url.stopAccessingSecurityScopedResource()
|
|
||||||
securityScopeUrl = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public func getSaveDirectory() -> URL {
|
|
||||||
|
|
||||||
// Not supported on iOS
|
|
||||||
#if os(macOS)
|
|
||||||
if let securityScopeUrl = securityScopeUrl {
|
|
||||||
log("[SaveFilesManager] Using existing security scope URL: \(securityScopeUrl)")
|
|
||||||
return securityScopeUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if let bookmarkData = UserDefaults.standard.data(forKey: UserDefaultsKeys.saveFolderBookmark.rawValue) {
|
|
||||||
var isStale = false
|
|
||||||
|
|
||||||
do {
|
|
||||||
let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
|
||||||
|
|
||||||
if !isStale {
|
|
||||||
if url.startAccessingSecurityScopedResource() {
|
|
||||||
log("[SaveFilesManager] Successfully accessed security scoped resource: \(url)")
|
|
||||||
|
|
||||||
securityScopeUrl = url
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log("[SaveFilesManager] Bookmark is stale, using default downloads folder.")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
log("[SaveFilesManager] Failed to resolve bookmark: \(error), using default downloads folder.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .downloadsDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("[SaveFilesManager] Failed to get downloads directory: \(error)")
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// Return the documents directory for iOS
|
|
||||||
do {
|
|
||||||
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).resolvingSymlinksInPath()
|
|
||||||
} catch {
|
|
||||||
fatalError("Failed to get documents directory: \(error)")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Returns the EXIF original date of an image file, adjusted to the current time zone.
|
|
||||||
private func exifOriginalDate(from url: URL) -> Date? {
|
|
||||||
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
||||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any],
|
|
||||||
let exif = properties[kCGImagePropertyExifDictionary] as? [CFString: Any],
|
|
||||||
let dateString = exif[kCGImagePropertyExifDateTimeOriginal] as? String
|
|
||||||
else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.dateFormat = "yyyy:MM:dd HH:mm:ss"
|
|
||||||
formatter.timeZone = TimeZone.current // Use the current device time zone
|
|
||||||
return formatter.date(from: dateString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamps to a given file or directory.
|
|
||||||
private func applyEXIFTimestamps(at url: URL) {
|
|
||||||
let fm = FileManager.default
|
|
||||||
var isDir: ObjCBool = false
|
|
||||||
guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { return }
|
|
||||||
|
|
||||||
if isDir.boolValue {
|
|
||||||
// It's a directory — enumerate files
|
|
||||||
if let enumerator = fm.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey]) {
|
|
||||||
for case let fileURL as URL in enumerator {
|
|
||||||
guard (try? fileURL.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else { continue }
|
|
||||||
applyEXIFTimestamp(to: fileURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// It's a single file
|
|
||||||
applyEXIFTimestamp(to: url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Applies EXIF timestamp (if available) to a single file
|
|
||||||
private func applyEXIFTimestamp(to fileURL: URL) {
|
|
||||||
guard let exifDate = exifOriginalDate(from: fileURL) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the "creation date"
|
|
||||||
try? FileManager.default.setAttributes([.creationDate: exifDate], ofItemAtPath: fileURL.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ import IOKit.pwr_mgt
|
|||||||
#elseif os(iOS)
|
#elseif os(iOS)
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
|
import LUI
|
||||||
|
|
||||||
class SleepManager {
|
class SleepManager {
|
||||||
public static let shared = SleepManager()
|
public static let shared = SleepManager()
|
||||||
|
|||||||
@@ -5,17 +5,20 @@
|
|||||||
// Created by Leon Böttger on 22.08.25.
|
// Created by Leon Böttger on 22.08.25.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
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] = [:]
|
||||||
|
|
||||||
private let trustedKeysKey = "com.leonboettger.quickdrop.identity.trustedKeys"
|
private let trustedKeysKey = "com.leonboettger.quickdrop.identity.trustedKeys"
|
||||||
|
private let pendingPairingQueue = DispatchQueue(label: "TrustStore.pairingPending")
|
||||||
|
private var pendingPairingTrust: [String: PendingPairingTrust] = [:]
|
||||||
|
private let pendingPairingTTL: TimeInterval = 5 * 60
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
loadTrustedCertificates()
|
loadTrustedCertificates()
|
||||||
@@ -25,29 +28,46 @@ class TrustStore: ObservableObject {
|
|||||||
// MARK: - Persistence
|
// MARK: - Persistence
|
||||||
|
|
||||||
private func loadTrustedCertificates() {
|
private func loadTrustedCertificates() {
|
||||||
guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
|
do {
|
||||||
|
if let data = try KeychainStore.loadData(account: trustedKeysKey) {
|
||||||
|
trustedCertificates = try JSONDecoder().decode([String: TrustedCertificate].self, from: data)
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("[TrustStore] Failed to load trusted certificates from keychain: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let dict = legacyTrustedCertificates() else {
|
||||||
trustedCertificates = [:]
|
trustedCertificates = [:]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var decoded: [String: TrustedCertificate] = [:]
|
trustedCertificates = dict
|
||||||
for (key, data) in dict {
|
|
||||||
if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
|
do {
|
||||||
decoded[key] = cert
|
try persistTrustedCertificates()
|
||||||
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
|
print("[TrustStore] Migrated trusted certificates from shared defaults to keychain.")
|
||||||
|
} catch {
|
||||||
|
print("[TrustStore] Failed to migrate trusted certificates to keychain: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trustedCertificates = decoded
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func saveTrustedCertificates() {
|
private func saveTrustedCertificates() {
|
||||||
var dict: [String: Data] = [:]
|
do {
|
||||||
for (key, cert) in trustedCertificates {
|
try persistTrustedCertificates()
|
||||||
if let data = try? JSONEncoder().encode(cert) {
|
AppGroup.appGroupUD.removeObject(forKey: trustedKeysKey)
|
||||||
dict[key] = data
|
notifyTrustedCertificatesDidChange()
|
||||||
|
} catch {
|
||||||
|
print("[TrustStore] Failed to save trusted certificates to keychain: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppGroup.appGroupUD.set(dict, forKey: trustedKeysKey)
|
|
||||||
|
|
||||||
|
private func notifyTrustedCertificatesDidChange() {
|
||||||
|
NotificationCenter.default.post(name: Self.trustedCertificatesDidChange, object: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -58,30 +78,172 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Pending Pairing Trust
|
||||||
|
|
||||||
|
func registerPendingPairingTrust(secretIdHex: String, pinCode: String, useCase: PairingUseCase) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
let trimmedPin = pinCode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !normalized.isEmpty, !trimmedPin.isEmpty else { return }
|
||||||
|
|
||||||
|
let pinHash = Self.pinHash(for: trimmedPin)
|
||||||
|
pendingPairingQueue.sync {
|
||||||
|
pendingPairingTrust[normalized] = PendingPairingTrust(
|
||||||
|
useCase: useCase,
|
||||||
|
pinHash: pinHash,
|
||||||
|
createdAt: Date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmPendingPairingTrust(secretIdHex: String, useCase: PairingUseCase, pinHash: Data) -> Bool {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard !normalized.isEmpty, !pinHash.isEmpty else { return false }
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
|
return pendingPairingQueue.sync {
|
||||||
|
guard let pending = pendingPairingTrust[normalized] else { return false }
|
||||||
|
pendingPairingTrust.removeValue(forKey: normalized)
|
||||||
|
let expired = now.timeIntervalSince(pending.createdAt) > pendingPairingTTL
|
||||||
|
if expired {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard pending.useCase == useCase else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pending.pinHash == pinHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearPendingPairingTrust(secretIdHex: String) {
|
||||||
|
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard !normalized.isEmpty else { return }
|
||||||
|
|
||||||
|
_ = pendingPairingQueue.sync {
|
||||||
|
pendingPairingTrust.removeValue(forKey: normalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
struct TrustedCertificate: Codable, Equatable {
|
struct TrustedCertificate: Codable, Equatable {
|
||||||
|
|
||||||
// warning: do not rename for persistence
|
// warning: do not rename for persistence
|
||||||
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 {
|
||||||
|
let useCase: PairingUseCase
|
||||||
|
let pinHash: Data
|
||||||
|
let createdAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func persistTrustedCertificates() throws {
|
||||||
|
let data = try JSONEncoder().encode(trustedCertificates)
|
||||||
|
try KeychainStore.saveData(data, account: trustedKeysKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func legacyTrustedCertificates() -> [String: TrustedCertificate]? {
|
||||||
|
guard let dict = AppGroup.appGroupUD.dictionary(forKey: trustedKeysKey) as? [String: Data] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded: [String: TrustedCertificate] = [:]
|
||||||
|
for (key, data) in dict {
|
||||||
|
if let cert = try? JSONDecoder().decode(TrustedCertificate.self, from: data) {
|
||||||
|
decoded[key] = cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func pinHash(for pinCode: String) -> Data {
|
||||||
|
let data = Data(pinCode.utf8)
|
||||||
|
let digest = SHA256.hash(data: data)
|
||||||
|
return Data(digest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,62 +6,42 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import ZIPFoundation
|
||||||
|
|
||||||
class Zip {
|
final class Zip {
|
||||||
|
|
||||||
static func createAtTemporaryDirectory(zipFilename: String, zipExtension: String = "zip", fromDirectory directoryURL: URL) throws -> URL {
|
static func createAtTemporaryDirectory(
|
||||||
|
zipFilename: String,
|
||||||
|
zipExtension: String = "zip",
|
||||||
|
fromDirectory directoryURL: URL
|
||||||
|
) throws -> URL {
|
||||||
|
|
||||||
let finalURL = FileManager.default.temporaryDirectory
|
guard directoryURL.isDirectory else { throw CreateZipError.urlNotADirectory(directoryURL) }
|
||||||
|
|
||||||
|
let zipFinalURL = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent(zipFilename)
|
.appendingPathComponent(zipFilename)
|
||||||
.appendingPathExtension(zipExtension)
|
.appendingPathExtension(zipExtension)
|
||||||
|
|
||||||
return try createAt(
|
|
||||||
zipFinalURL: finalURL,
|
|
||||||
fromDirectory: directoryURL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static func createAt(zipFinalURL: URL, fromDirectory directoryURL: URL) throws -> URL {
|
|
||||||
|
|
||||||
guard directoryURL.isDirectory else {
|
|
||||||
throw CreateZipError.urlNotADirectory(directoryURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileManagerError: Swift.Error?
|
|
||||||
var coordinatorError: NSError?
|
|
||||||
let coordinator = NSFileCoordinator()
|
|
||||||
|
|
||||||
coordinator.coordinate(readingItemAt: directoryURL, options: .forUploading, error: &coordinatorError) { zipCreatedURL in
|
|
||||||
do {
|
|
||||||
// clean if file exists
|
|
||||||
if FileManager.default.fileExists(atPath: zipFinalURL.path) {
|
if FileManager.default.fileExists(atPath: zipFinalURL.path) {
|
||||||
try FileManager.default.removeItem(at: zipFinalURL)
|
try FileManager.default.removeItem(at: zipFinalURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)
|
try FileManager.default.zipItem(
|
||||||
|
at: directoryURL,
|
||||||
} catch {
|
to: zipFinalURL,
|
||||||
fileManagerError = error
|
shouldKeepParent: true,
|
||||||
}
|
compressionMethod: .deflate
|
||||||
}
|
)
|
||||||
|
|
||||||
if let error = coordinatorError ?? fileManagerError {
|
|
||||||
throw CreateZipError.failedToCreateZIP(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return zipFinalURL
|
return zipFinalURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CreateZipError: Error {
|
||||||
enum CreateZipError: Swift.Error {
|
|
||||||
case urlNotADirectory(URL)
|
case urlNotADirectory(URL)
|
||||||
case failedToCreateZIP(Swift.Error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension URL {
|
||||||
extension URL {
|
|
||||||
var isDirectory: Bool {
|
var isDirectory: Bool {
|
||||||
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
|
(try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.leonboettger.neardrop</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>quickdrop</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>UIDesignRequiresCompatibility</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "212cf044c2b0bab89578ca93a3666a656e0c44fb54a69f813c4cc4fae07827d7",
|
"originHash" : "85c9b997510f22b553d79f68fb6d51d149ac9324df643dbf1475023b39da33b3",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "applewatchscreensize",
|
"identity" : "applewatchscreensize",
|
||||||
@@ -33,8 +33,17 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/leif-ibsen/BigInt",
|
"location" : "https://github.com/leif-ibsen/BigInt",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "afb70a0038bfbba845271b60fa9a58d5840f8017",
|
"revision" : "7c7b1be2f6714ce03833879b0aab4b20293c0207",
|
||||||
"version" : "1.21.0"
|
"version" : "1.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "devicekit",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/devicekit/DeviceKit",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "56b997e8a61707218f9af09f32b2a1d1806fd792",
|
||||||
|
"version" : "5.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,7 +52,7 @@
|
|||||||
"location" : "https://github.com/leonboe1/LUI.git",
|
"location" : "https://github.com/leonboe1/LUI.git",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "5b2fe7161069c2314c5be17810ec7471b9decc8a"
|
"revision" : "44013e8ef77d8e0d18e13f449cfe3775f1abb31a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -55,15 +64,6 @@
|
|||||||
"version" : "26.10.0"
|
"version" : "26.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identity" : "rndevicename",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/souzainf3/RNDeviceName",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "68ada02a7135381425b01778aeb8719ce3bf8f62",
|
|
||||||
"version" : "2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "swift-cross-kit-types",
|
"identity" : "swift-cross-kit-types",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -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" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
|
"revision" : "55d7a1cc5666b85c13464aea1c4b4a90feccb4c8",
|
||||||
"version" : "1.31.0"
|
"version" : "1.38.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,7 +124,16 @@
|
|||||||
"location" : "https://github.com/siteline/swiftui-introspect",
|
"location" : "https://github.com/siteline/swiftui-introspect",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "9bedf1e79c7b7c34b35b74bef126f56d2400ef7f"
|
"revision" : "aee624c87acee41e1a421f7825b468d38bbfbd6c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "zipfoundation",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/weichsel/ZIPFoundation.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "22787ffb59de99e5dc1fbfe80b19c97a904ad48d",
|
||||||
|
"version" : "0.9.20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1640"
|
LastUpgradeVersion = "2600"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -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
|
||||||
@@ -50,6 +68,9 @@
|
|||||||
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../ConfigSynced.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2600"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EB3B9EF02EF7150400D6704E"
|
||||||
|
BuildableName = "QuickDropGitHub.app"
|
||||||
|
BlueprintName = "QuickDropGitHub"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</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">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EB3B9EF02EF7150400D6704E"
|
||||||
|
BuildableName = "QuickDropGitHub.app"
|
||||||
|
BlueprintName = "QuickDropGitHub"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EB3B9EF02EF7150400D6704E"
|
||||||
|
BuildableName = "QuickDropGitHub.app"
|
||||||
|
BlueprintName = "QuickDropGitHub"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2600"
|
||||||
|
version = "2.0">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EB3B9F4A2EF7151900D6704E"
|
||||||
|
BuildableName = "QuickDropShareExtensionGitHub.appex"
|
||||||
|
BlueprintName = "QuickDropShareExtensionGitHub"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = ""
|
||||||
|
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||||
|
launchStyle = "0"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
askForAppToLaunch = "Yes"
|
||||||
|
launchAutomaticallySubstyle = "2">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "EB3B9F4A2EF7151900D6704E"
|
||||||
|
BuildableName = "QuickDropShareExtensionGitHub.appex"
|
||||||
|
BlueprintName = "QuickDropShareExtensionGitHub"
|
||||||
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -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>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1640"
|
LastUpgradeVersion = "2600"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
@@ -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"
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
ReferencedContainer = "container:QuickDrop.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<StoreKitConfigurationFileReference
|
||||||
|
identifier = "../../ConfigSynced.storekit">
|
||||||
|
</StoreKitConfigurationFileReference>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "Release"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1640"
|
LastUpgradeVersion = "2600"
|
||||||
wasCreatedForAppExtension = "YES"
|
wasCreatedForAppExtension = "YES"
|
||||||
version = "2.0">
|
version = "2.0">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1640"
|
LastUpgradeVersion = "2600"
|
||||||
wasCreatedForAppExtension = "YES"
|
wasCreatedForAppExtension = "YES"
|
||||||
version = "2.0">
|
version = "2.0">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
|
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
import BezelNotification
|
import BezelNotification
|
||||||
|
import Combine
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Network
|
import Network
|
||||||
import StoreKit
|
import StoreKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
import LUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, NSWindowDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, NSWindowDelegate {
|
||||||
@@ -25,79 +27,125 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
private var sheetAttachedWindow: NSWindow? = nil
|
private var sheetAttachedWindow: NSWindow? = nil
|
||||||
private var errorAlertHandler = ErrorAlertHandler.shared
|
private var errorAlertHandler = ErrorAlertHandler.shared
|
||||||
|
|
||||||
private var iapManager: IAPManager?
|
|
||||||
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
|
||||||
|
if !Settings.sharedInstance.appLaunchedBefore {
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Opening Welcome Screen") }
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
// open welcome screen in next cycle (await LUIInit)
|
||||||
|
self.openMainWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// user installed the app after the IAP was implemented, set the user as eligible for IAP
|
||||||
|
Settings.sharedInstance.isEligibleForIap = true
|
||||||
|
LUIInit(configuration: configuration)
|
||||||
|
} else {
|
||||||
|
// app launched before
|
||||||
|
#if GITHUB
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Downloaded from GitHub.") }
|
||||||
|
#else
|
||||||
|
// user installed the app before the IAP was implemented, grant the plus version
|
||||||
|
if !Settings.sharedInstance.isEligibleForIap {
|
||||||
|
runAfter(seconds: 0.1) { log("[AppDelegate] Granting QuickDrop+ for old user") }
|
||||||
|
UserDefaults.standard.set(true, forKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
BezelNotification.show(messageText: "ReadyToReceive".localized(), icon: .receiveIcon)
|
||||||
|
|
||||||
|
// only start receiving immediately for existing user, for new users we want to delay the permission prompt
|
||||||
|
LUIInit(configuration: configuration)
|
||||||
|
startReceiving()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Menu Bar
|
||||||
|
|
||||||
let menu = NSMenu()
|
let menu = NSMenu()
|
||||||
|
|
||||||
let visibleItem = NSMenuItem(
|
let visibleItem = NSMenuItem(
|
||||||
title: "VisibleToEveryone".localized(),
|
title: getDefaultVisibleLabel(),
|
||||||
action: nil,
|
action: #selector(visibleMenuItemSelected),
|
||||||
keyEquivalent: ""
|
keyEquivalent: ""
|
||||||
)
|
)
|
||||||
|
visibleItem.target = self
|
||||||
self.visibleItem = visibleItem
|
self.visibleItem = visibleItem
|
||||||
menu.addItem(visibleItem)
|
menu.addItem(visibleItem)
|
||||||
|
|
||||||
menu.addItem(withTitle: String(format: "DeviceName".localized(), arguments: [Host.current().localizedName ?? "Mac"]), action: nil, keyEquivalent: "")
|
|
||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
|
let sendClipboardItem = NSMenuItem(title: "SendClipboard".localized(), action: #selector(sendClipboard), keyEquivalent: "")
|
||||||
|
sendClipboardItem.image = NSImage(systemSymbolName: "doc.on.clipboard", accessibilityDescription: nil)
|
||||||
menu.addItem(sendClipboardItem)
|
menu.addItem(sendClipboardItem)
|
||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openWelcomeScreen), keyEquivalent: "")
|
let userManualItem = NSMenuItem(title: "UserManual".localized(), action: #selector(openMainWindow), keyEquivalent: "")
|
||||||
|
userManualItem.image = NSImage(systemSymbolName: "gear", accessibilityDescription: nil)
|
||||||
menu.addItem(userManualItem)
|
menu.addItem(userManualItem)
|
||||||
|
|
||||||
menu.addItem(withTitle: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
let quitItem = NSMenuItem(title: "Quit".localized(), action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
||||||
|
quitItem.image = NSImage(systemSymbolName: "xmark", accessibilityDescription: nil)
|
||||||
|
menu.addItem(quitItem)
|
||||||
|
|
||||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||||||
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||||
statusItem?.menu = menu
|
statusItem?.menu = menu
|
||||||
statusItem?.behavior = .removalAllowed
|
statusItem?.behavior = .removalAllowed
|
||||||
|
|
||||||
iapManager = IAPManager.sharedInstance
|
|
||||||
iapManager?.startObserving()
|
|
||||||
|
|
||||||
log("[AppDelegate] Downloaded directly: \(DistributionDetector.isDirectDistributionEnabled)")
|
// MARK: Nearby Connections Callbacks
|
||||||
|
|
||||||
// app did not lauch before
|
NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
|
||||||
if !UserDefaults.standard.bool(forKey: UserDefaultsKeys.appLaunchedBefore.rawValue) {
|
if isConnected {
|
||||||
log("[AppDelegate] Opening Welcome Screen")
|
self.statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
||||||
// open welcome screen
|
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
||||||
openWelcomeScreen()
|
|
||||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.appLaunchedBefore.rawValue)
|
|
||||||
|
|
||||||
// user installed the app after the IAP was implemented, set the user as eligible for IAP
|
|
||||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.isEligibleForIap.rawValue)
|
|
||||||
} else {
|
|
||||||
// app launched before
|
|
||||||
|
|
||||||
// user installed the app before the IAP was implemented, grant the plus version
|
|
||||||
if !UserDefaults.standard.bool(forKey: UserDefaultsKeys.isEligibleForIap.rawValue) {
|
|
||||||
log("[AppDelegate] Granting QuickDrop+ for old user")
|
|
||||||
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.plusVersion.rawValue)
|
|
||||||
} else {
|
|
||||||
if !isPlusVersion() {
|
|
||||||
log("[AppDelegate] New user - QuickDrop+ not available")
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log("[AppDelegate] QuickDrop+ available")
|
self.statusItem?.button?.image = NSImage(named: "MenuBarIconSlash")
|
||||||
|
self.visibleItem?.title = "NoNetworkConnection".localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BezelNotification.show(messageText: "ReadyToReceive".localized(), icon: .receiveIcon)
|
NearbyConnectionManager.shared.changedDeviceNameCallback = {
|
||||||
|
self.visibleItem?.title = self.getDefaultVisibleLabel()
|
||||||
}
|
}
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
setupAndroidAirDropModeDetection()
|
||||||
|
requestBluetoothPermissionForExistingUserIfNeeded()
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Logging
|
||||||
|
|
||||||
|
log("[AppDelegate] Application did finish launching")
|
||||||
|
log("[AppDelegate] Currently running apps: \(NSWorkspace.shared.runningApplications.map { $0.localizedName ?? "-" })")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func startReceiving() {
|
||||||
|
guard receiveModel == nil else {
|
||||||
|
log("[AppDelegate] startReceiving called while receiver is already active. Ignoring duplicate call.")
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in
|
receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in
|
||||||
if shouldOpen {
|
if shouldOpen {
|
||||||
@@ -112,25 +160,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ClipboardSyncCoordinator.shared.start()
|
||||||
NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
|
|
||||||
if isConnected {
|
|
||||||
self.statusItem?.button?.image = NSImage(named: "MenuBarIcon")
|
|
||||||
self.visibleItem?.title = "VisibleToEveryone".localized()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
self.statusItem?.button?.image = NSImage(named: "MenuBarIconSlash")
|
|
||||||
self.visibleItem?.title = "NoNetworkConnection".localized()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Application did finish launching")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
if let window = notification.object as? NSWindow, window == welcomeWindow {
|
if let window = notification.object as? NSWindow, window == welcomeWindow {
|
||||||
log("Welcome window closed")
|
log("[AppDelegate] Welcome window closed")
|
||||||
welcomeWindow = nil
|
welcomeWindow = nil
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
}
|
}
|
||||||
@@ -138,37 +174,67 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
|
|
||||||
|
|
||||||
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
|
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool {
|
||||||
openWelcomeScreen()
|
self.openMainWindow()
|
||||||
statusItem?.isVisible = true
|
statusItem?.isVisible = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
iapManager?.stopObserving()
|
ClipboardSyncCoordinator.shared.stop()
|
||||||
|
AndroidAirDropModeDetector.shared.stop()
|
||||||
|
NearbyConnectionManager.shared.stopDeviceDiscovery()
|
||||||
|
NearbyConnectionManager.shared.becomeInvisible()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func application(_ application: NSApplication, open urls: [URL]) {
|
func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
|
||||||
|
log("[AppDelegate] application(open:): Received URLs: \(urls)")
|
||||||
|
|
||||||
for url in urls {
|
for url in urls {
|
||||||
if url.scheme == "quickdrop", url.host == "sendLog" {
|
if url.scheme == "quickdrop" {
|
||||||
sendLoggingString() // this time it runs in the main app
|
switch url.host {
|
||||||
|
case "sendLog":
|
||||||
|
// If we're in an extension, redirect to the main app
|
||||||
|
if Bundle.main.bundlePath.hasSuffix(".appex") {
|
||||||
|
log("[AppDelegate] sendLoggingString: in extension, redirecting to main app")
|
||||||
|
|
||||||
|
if let url = URL(string: "quickdrop://sendLog") {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LogExportPresenter.showUploadLogsAlert()
|
||||||
if url.scheme == "quickdrop", url.host == "openLog", let url = LogManager.sharedInstance.logFileURL {
|
case "openLog":
|
||||||
log("Opening log file: \(url)")
|
if let url = LogManager.sharedInstance.logFileURL {
|
||||||
// open folder containing the log file
|
log("[AppDelegate] Opening log file: \(url)")
|
||||||
NSWorkspace.shared.activateFileViewerSelecting([url])
|
NSWorkspace.shared.activateFileViewerSelecting([url])
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
log("[AppDelegate] No log file found to open.")
|
||||||
|
}
|
||||||
|
case "mockTransferFailure":
|
||||||
|
ErrorAlertHandler.shared.showErrorAlert(for: "Test Device", error: NearbyError.inputOutput)
|
||||||
|
case "removeData":
|
||||||
|
Settings.sharedInstance.deleteAllUserDefaults()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,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
|
||||||
@@ -191,13 +261,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let sharingService = NSSharingService(named: NSSharingService.Name("com.leonboettger.neardrop.ShareExtension"))
|
sendToSharingService(items: [clipboardString])
|
||||||
sharingService?.perform(withItems: [clipboardString])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@objc func openWelcomeScreen() {
|
@objc func openMainWindow() {
|
||||||
|
showMainWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func showMainWindow(selectedTab: Tab? = nil) {
|
||||||
|
if let selectedTab {
|
||||||
|
welcomeNavigationState.select(selectedTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if let welcomeWindow, !Settings.sharedInstance.appLaunchedBefore {
|
||||||
|
runAfter(seconds: 0.2) {
|
||||||
|
IntroductionSheetManager.sharedInstance.openIntroductionWindow(on: welcomeWindow, startReceiving: self.startReceiving) {
|
||||||
|
self.startReceiving()
|
||||||
|
Settings.sharedInstance.appLaunchedBefore = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSApp.setActivationPolicy(.regular)
|
NSApp.setActivationPolicy(.regular)
|
||||||
|
|
||||||
@@ -214,11 +302,12 @@ 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(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 400, height: 280),
|
contentRect: NSRect(x: 0, y: 0, width: WelcomeScreen.width, height: WelcomeScreen.height),
|
||||||
styleMask: [.titled, .closable],
|
styleMask: [.titled, .closable],
|
||||||
backing: .buffered,
|
backing: .buffered,
|
||||||
defer: false
|
defer: false
|
||||||
@@ -245,18 +334,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
if let window = plusWindow, window.isVisible {
|
if let window = plusWindow, window.isVisible {
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
log("Plus screen already open, bringing to front")
|
log("[AppDelegate] Plus screen already open, bringing to front")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Opening Plus screen")
|
log("[AppDelegate] Opening Plus screen")
|
||||||
|
|
||||||
// Create the welcome screen SwiftUI view
|
// Create the welcome screen SwiftUI view
|
||||||
let plusView = PlusView(closeView: {
|
let plusView = GetPlusViewV4(showSheet: Binding(get: { self.plusWindow != nil }, set: { newValue in
|
||||||
log("Closing plus screen")
|
if !newValue {
|
||||||
self.plusWindow?.close()
|
self.plusWindow?.close()
|
||||||
self.plusWindow = nil
|
self.plusWindow = nil
|
||||||
})
|
}
|
||||||
|
}), hasSheet: false)
|
||||||
|
|
||||||
|
let plusViewWidth: CGFloat = 750
|
||||||
|
let plusViewHeight: CGFloat = 750
|
||||||
|
|
||||||
// Create an NSWindow to host the SwiftUI view
|
// Create an NSWindow to host the SwiftUI view
|
||||||
let window = NSWindow(
|
let window = NSWindow(
|
||||||
@@ -285,7 +378,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
// Ensure the window is always on top
|
// Ensure the window is always on top
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
window.level = .normal
|
window.level = .floating
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -327,17 +420,75 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
// MARK: - Helper Functions
|
// MARK: - Helper Functions
|
||||||
|
|
||||||
private func performDeviceToDeviceCheck() {
|
private func performDeviceToDeviceCheck() {
|
||||||
let scanner = DeviceToDeviceHeuristicScanner()
|
let scanner = DeviceToDeviceHeuristicScanner.shared
|
||||||
scanner.scan { allowed in
|
scanner.scan { allowed in
|
||||||
if allowed {
|
if allowed {
|
||||||
log("✅ Device-to-device likely allowed (peer responded on LAN).")
|
log("[AppDelegate] ✅ Device-to-device likely allowed (peer responded on LAN).")
|
||||||
ErrorAlertHandler.shared.closeApIsolationAlert()
|
ErrorAlertHandler.shared.closeApIsolationAlert()
|
||||||
} else {
|
} else {
|
||||||
log("❌ Device-to-device check failed. Informing user...")
|
log("[AppDelegate] ❌ Device-to-device check failed. Informing user...")
|
||||||
ErrorAlertHandler.shared.openAlert(type: .ApIsolation)
|
ErrorAlertHandler.shared.openAlert(type: .ApIsolation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
|
||||||
|
return String(format: "VisibleAs".localized(), name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TransferInfo {
|
struct TransferInfo {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 585 KiB |
|
Before Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 752 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "1024-1024 1.png",
|
"filename" : "1024-1024 1 2.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
@@ -12,15 +12,6 @@
|
|||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x"
|
"scale" : "3x"
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename" : "1024-1024.png",
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "mac",
|
|
||||||
"scale" : "2x"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 752 KiB After Width: | Height: | Size: 672 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Plain.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -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 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "FileTransfer Kopie-1024.png",
|
"filename" : "iDroidIcon-iOS-Default-1024x1024@1x.png",
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
|
|||||||