218 Commits
Author SHA1 Message Date
Leon Böttger dd9462d9f5 Bump build number 2026-05-23 14:46:52 +02:00
Leon Böttger 238769656a Avoid link local IPv6 addresses 2026-05-20 19:38:23 +02:00
Leon Böttger fd29254e38 Extract NearbyBonjourServiceResolver 2026-05-20 19:21:54 +02:00
Leon Böttger 6b0e14de3e More robust outgoing connection 2026-05-19 21:48:26 +02:00
Leon Böttger db70d40e43 Fix clipboard notification occasionally not appearing 2026-05-19 21:29:18 +02:00
Leon Böttger 4394b78242 Update version 2026-05-18 21:18:56 +02:00
Leon Böttger 488f1a14b2 Only poll clipboard if clipboard sync was enabled 2026-05-16 14:51:07 +02:00
Leon Böttger 72bfd88161 Add clipboard sync pairing 2026-05-16 14:46:22 +02:00
Leon Böttger 2f6b0ff5ba Add clipboard sync notification 2026-05-14 15:49:50 +02:00
Leon Böttger 5fdc5f8a1b Add Clipboard Listener 2026-05-14 14:44:17 +02:00
Leon Böttger ee091f60cd Update Screenshots 2026-05-14 10:54:45 +02:00
Leon Böttger 6c00c0f321 Package update 2026-05-14 10:54:32 +02:00
Leon Böttger 65a0ab3226 Improve iOS UI 2026-05-02 19:31:49 +02:00
Leon Böttger f5ca60a0ad Bump version 2026-05-01 10:43:40 +02:00
Leon Böttger ce16bca415 Fix zipping large file freezes app 2026-04-27 21:03:04 +02:00
Leon Böttger e6d1c7b1a3 Add device name lookup for unnamed devices 2026-04-27 20:29:35 +02:00
Leon Böttger 84d2150893 Always try to send logs on contact 2026-04-17 21:02:16 +02:00
Leon Böttger 007784c483 More robust connection 2026-04-17 19:34:11 +02:00
Leon Böttger 78cf2309f9 Package updates 2026-04-12 15:14:06 +02:00
Leon Böttger 976fc280db NearbyConnection: Fix missing disconnect handler 2026-04-10 19:34:18 +02:00
Leon Böttger 02ab04c69e Fix toast view liquid glass not updating background 2026-04-05 11:24:08 +02:00
Leon Böttger fcbe99dd85 Formatting 2026-04-05 11:05:55 +02:00
Leon Böttger feaf557ca2 Fix outgoing transfer flakiness 2026-04-03 19:19:22 +02:00
Leon Böttger 9a488a2ae2 Refactoring 2026-04-01 19:00:24 +02:00
Leon Böttger 80b81ae228 Fix ToastView progress bar animation 2026-03-31 21:30:17 +02:00
Leon Böttger 0112e512c0 Add notification sync documentation 2026-03-31 21:19:09 +02:00
Leon Böttger 0d0b720114 Bump version number 2026-03-25 08:44:45 +01:00
Leon Böttger 13f3073077 Update Mac background colors 2026-03-25 07:19:17 +01:00
Leon Böttger ff39ba4c7d Bump version 2026-03-22 15:38:39 +01:00
Leon Böttger 245358ae96 Send keepalive ack during handshake 2026-03-21 21:51:19 +01:00
Leon Böttger 429069f372 Don't send keepalive too early 2026-03-21 21:16:26 +01:00
Leon Böttger 04e056da02 Ignore keepalive during handshake 2026-03-21 21:09:54 +01:00
Leon Böttger 50d4d4ee92 Change toast view close button position 2026-03-21 13:47:20 +01:00
Leon Böttger 86b7e4ca24 Safer dataTransferInactivityTimer handling 2026-03-21 11:56:05 +01:00
Leon Böttger d3a95c068f Improve logging 2026-03-21 11:32:41 +01:00
Leon Böttger e3cbc19f11 Re-add openLog 2026-03-21 11:15:15 +01:00
Leon Böttger c46f6eedbc Extract pairing proto 2026-03-18 20:05:07 +01:00
Leon Böttger ea213906d6 Fix cannot share screenshots 2026-03-17 21:05:01 +01:00
Leon Böttger 4700a9c56e Package updates 2026-03-16 21:27:25 +01:00
Leon Böttger 4fe30da52b Improve Strings 2026-03-16 21:27:02 +01:00
Leon Böttger bd93961c59 Ensure P256 derived field length 2026-03-16 21:26:20 +01:00
Leon Böttger f244572508 Improve Strings 2026-03-16 19:10:20 +01:00
Leon Böttger d4ea143d51 UI Improvements 2026-03-16 07:22:35 +01:00
Leon Böttger 2dbc46f792 Fix init order 2026-03-15 21:18:33 +01:00
Leon Böttger f61be69507 Add QR code based pairing 2026-03-15 21:00:12 +01:00
Leon Böttger 82f40d780f PIN check for pairing 2026-03-15 15:57:56 +01:00
Leon Böttger 606082d76d Improve toast view UI 2026-03-15 14:56:53 +01:00
Leon Böttger 2233699ece Toast View adjustment 2026-03-15 13:59:08 +01:00
Leon Böttger 130935c01d Use ToastView for notification setup 2026-03-14 21:20:45 +01:00
Leon Böttger 74226d2fc3 Order TrustedDevicesView by date 2026-03-14 19:35:53 +01:00
Leon Böttger e895e7a68e Better device name fallback 2026-03-14 11:36:44 +01:00
Leon Böttger 843ed9fe4f Use Keychain for trusted devices 2026-03-14 11:33:27 +01:00
Leon Böttger 2ef3bc3b44 Add auth for incoming transfers 2026-03-14 11:15:15 +01:00
Leon Böttger 216e2cf851 Add nync to protobuf 2026-03-14 08:26:00 +01:00
Leon Böttger d63665136b Remove liquid glass force refresh 2026-03-13 21:20:50 +01:00
Leon Böttger 42c101605f Show unknown devicrd 2026-03-13 19:48:36 +01:00
Leon Böttger fd1dfb4f4a Always allow incoming notifications 2026-03-13 19:09:03 +01:00
Leon Böttger 767a5c5706 Bump build number 2026-03-11 21:25:28 +01:00
Leon Böttger 8e78f58b5e Fix log sending from share extension 2026-03-11 21:08:56 +01:00
Leon Böttger 935bdc5a65 Fix liquid glass background refresh 2026-03-11 21:05:31 +01:00
Leon Böttger f559a6ac9d Improve logging 2026-03-11 20:48:19 +01:00
Leon Böttger e853ba2b36 Improve logging 2026-03-11 20:23:13 +01:00
Leon Böttger 4621395a87 Package updates 2026-03-11 19:58:26 +01:00
Leon Böttger 55e2a72a90 Fix notification sync sematics 2026-03-11 19:05:38 +01:00
Leon Böttger 159e83179b Improve Strings 2026-03-11 19:00:57 +01:00
Leon Böttger 6a77e4aba6 Bump version 2026-03-11 18:46:15 +01:00
Leon Böttger ffe3ac7a4a Bump version 2026-03-08 16:17:50 +01:00
Leon Böttger 2dbe945f3e Better out-of-frame detection 2026-03-08 16:15:21 +01:00
Leon Böttger e828edd6ae Better error handling 2026-03-08 15:44:25 +01:00
Leon Böttger 7fbe254118 Ignore duplicate sequence numbers 2026-03-08 15:42:12 +01:00
Leon Böttger cffaabc076 Fail fast on outbound rejection 2026-03-08 15:39:17 +01:00
Leon Böttger 3736605509 Better frame duplication logic 2026-03-08 15:30:22 +01:00
Leon Böttger e0407d9398 Safer connection handling 2026-03-08 15:18:07 +01:00
Leon Böttger 3ed93e6079 Add liquid glass for toast view 2026-03-08 15:12:27 +01:00
Leon Böttger e1924e1919 Improve ACK logic 2026-03-08 14:40:16 +01:00
Leon Böttger c10facbedd Disconnect only after sending last frame 2026-03-08 14:32:07 +01:00
Leon Böttger a1b4a075a6 Support body / EOF frame separately 2026-03-08 14:27:31 +01:00
Leon Böttger bf7ad37d15 Recover files on early disconnect 2026-03-08 14:13:39 +01:00
Leon Böttger e68167788a Process partial frame on end of connection 2026-03-08 14:07:48 +01:00
Leon Böttger 48eb5be4a5 Add notification sync support 2026-03-08 13:49:07 +01:00
Leon Böttger a352e9ed1c Fix 2026-03-07 10:32:07 +01:00
Leon Böttger 0f8c9d254e Remove unused code 2026-03-01 10:54:03 +01:00
Leon Böttger be4d342894 Fix toast view appearance 2026-03-01 10:38:30 +01:00
Leon Böttger 811b991fcd Fix startup sequence 2026-02-26 20:51:01 +01:00
Leon Böttger d46b62d3de Refactoring 2026-02-23 19:15:14 +01:00
Leon Böttger 093390084f Fix toast view stutter 2026-02-22 19:37:10 +01:00
Leon Böttger b16556dd1b Fix toast view slide in animation 2026-02-22 19:18:36 +01:00
Leon Böttger f5d895ed56 Fix macOS 12 not visible on first start 2026-02-22 18:53:20 +01:00
Leon Böttger c489ef018f Fix toast view disappears if new connection comes right after 2026-02-22 16:12:02 +01:00
Leon Böttger 7e0a2e2830 Toast view cancel all connections with xmark 2026-02-19 21:35:14 +01:00
Leon Böttger 0b94a888b5 Fix cancel not hiding toast view 2026-02-19 21:29:36 +01:00
Leon Böttger 15677b7843 Fix toast view race condition 2026-02-19 21:23:18 +01:00
Leon Böttger 6401c1969c Allow multiple incoming connections 2026-02-19 20:53:48 +01:00
Leon Böttger bb1f28af0b Don't create multiple ReceiveModels in AppDelegate 2026-02-16 19:43:42 +01:00
Leon Böttger d367e0ff34 Fix accept button on macOS 12 2026-02-12 21:24:17 +01:00
Leon Böttger ec093cf302 Formatting 2026-02-12 18:59:57 +01:00
Leon Böttger edc09b048c Fix rename alert 2026-02-11 21:13:23 +01:00
Leon Böttger 9f15ee85f6 Improve toast view 2026-02-11 21:04:07 +01:00
Leon Böttger 10ef0c5a21 Always use fade as hide animation for toast view 2026-02-11 19:18:50 +01:00
Leon Böttger 0aa4fd6894 Improve UI 2026-02-11 19:14:19 +01:00
Leon Böttger 71bddf6541 Fix glitch on macOS 12 2026-02-11 19:05:39 +01:00
Leon Böttger 3ac2280dc6 UI improvements 2026-02-08 11:14:45 +01:00
Leon Böttger 8b965a7eb2 Fix toast could appear twice 2026-02-08 10:11:10 +01:00
Leon Böttger f11943475c Slide in for tast view 2026-02-08 08:41:59 +01:00
Leon Böttger a5781e6e96 Fix toast view dark mode 2026-02-07 21:29:27 +01:00
Leon Böttger ed20c9155e Replace alert with toast view 2026-02-07 15:15:26 +01:00
Leon Böttger b55d6ff07b UI improvements 2026-02-06 18:42:39 +01:00
Leon Böttger c86ed97bc2 Improve support link & logging 2026-02-05 20:54:59 +01:00
Leon Böttger eb32603c5f Fix issue that prevented sharing files 2026-02-02 20:47:34 +01:00
Leon Böttger 044753efb1 Present introduction as sheet window 2026-02-01 15:12:29 +01:00
Leon Böttger e8fdd0e94a Improve introduction view 2026-02-01 14:54:41 +01:00
Leon Böttger 09ef75d249 Refactoring 2026-01-31 21:21:32 +01:00
Leon Böttger 3551c1a006 Fix: Cannot save photos without creation date 2026-01-25 21:00:53 +01:00
Leon Böttger d748167f0e Refactoring 2026-01-25 20:31:34 +01:00
Leon Böttger 02e8af22ab Cleanup TrustedDevicesView 2026-01-25 14:52:52 +01:00
Leon Böttger 0e1fab8faa Improve strings 2026-01-25 13:41:31 +01:00
Leon Böttger 0396c56a31 Add support for WiFi password transfer & add FAQ 2026-01-25 13:30:04 +01:00
Leon Böttger ffdf667cc6 Refactoring 2026-01-23 21:31:25 +01:00
Leon Böttger cce84e9460 Prefer fixed port if possible 2026-01-23 20:58:45 +01:00
Leon Böttger b546370e87 Clean up after closing app 2026-01-18 18:50:22 +01:00
Leon Böttger fbd14c2280 Fix toast view cutoff 2026-01-18 11:31:37 +01:00
Leon Böttger bf1a8164d9 Update metadata 2026-01-18 11:16:58 +01:00
Leon Böttger 0a53ad79cb Fix toast view offset when transfer done 2026-01-15 21:32:03 +01:00
Leon Böttger 800fd23be3 Package updates 2026-01-15 21:17:14 +01:00
Leon Böttger 487a27a019 Simplify introduction 2026-01-08 21:00:43 +01:00
Leon Böttger c74858ea19 Add removeData URL scheme for debugging 2026-01-06 18:57:45 +01:00
Leon Böttger 40169334ff Fix connection drop for larger files 2026-01-01 12:03:50 +01:00
Leon Böttger ed213b4d3c Stop advertising when screen off or other user logged in 2025-12-26 19:08:05 +01:00
Leon Böttger 0697b94547 UI improvements 2025-12-26 18:18:12 +01:00
Leon Böttger e8fc6f719e Performance improvements 2025-12-26 17:50:08 +01:00
Leon Böttger 9dfe864677 Fix outgoing transfer to Windows 2025-12-26 15:41:22 +01:00
Leon Böttger a9e6ed1ddf Fix max device name chars 2025-12-26 13:42:46 +01:00
Leon Böttger 728385ff95 Add button to enable share extension 2025-12-26 13:18:54 +01:00
Leon Böttger 46cdec35d5 Improve strings 2025-12-26 11:43:47 +01:00
Leon Böttger daa1ab77df Fix connection to Windows 2025-12-26 11:36:24 +01:00
Leon Böttger 6766ef69d9 Add synced StoreKit file 2025-12-26 11:35:39 +01:00
Leon Böttger 7a964e35ca Improve strings 2025-12-26 11:35:27 +01:00
Leon Böttger 733ed8bec8 Automatically delete temporary files after file transfer 2025-12-21 13:48:31 +01:00
Leon Böttger 24a0202ae9 Improve strings for introduction 2025-12-21 11:23:16 +01:00
Leon Böttger 945fdcaf6e Show IntroductionView in separate window 2025-12-21 11:16:51 +01:00
Leon Böttger 7cdfdcd474 Fix edit device name alert for macOS 12 2025-12-20 21:05:01 +01:00
Leon Böttger ad1ab30b94 Add GitHub release target 2025-12-20 20:20:18 +01:00
Leon Böttger 4a8ba26c8c Update README 2025-12-20 15:50:02 +01:00
Leon Böttger c887ccb637 Improve macOS introduction 2025-12-20 15:48:39 +01:00
Leon Böttger fb13405247 Add Local Network Checker 2025-12-20 15:24:57 +01:00
Leon Böttger b61aecf315 Add local network capability for macOS 2025-12-20 14:19:42 +01:00
Leon Böttger 8a9bb0bdfe Fix: Delay local network permission prompt to introduction 2025-12-20 14:00:51 +01:00
Leon Böttger 1bd15acaef Improve macOS introduction 2025-12-20 13:36:30 +01:00
Leon Böttger 3b99eda41c macOS: Add new introduction 2025-12-20 12:12:12 +01:00
Leon Böttger 8393a7495f Fix ZIP creation for certain file types, improve UX for outgoing transfer 2025-12-18 21:31:36 +01:00
Leon Böttger a5e1030c1f Add ability to set custom device name 2025-12-18 20:37:44 +01:00
Leon Böttger 8aa3341595 Update version number 2025-12-16 18:47:32 +01:00
Leon Böttger f5e3c88daf Add StoreKit 2 2025-12-02 19:34:38 +01:00
Leon Böttger 488f898d7b FIx: Cannot save images to photos if custom directory chosen 2025-11-29 18:39:52 +01:00
Leon Böttger 9e7278d327 Fix: iOS share extension not detecting new devices after a while #2 2025-11-22 18:52:02 +01:00
Leon Böttger fdc19b6f62 Fix: iOS share extension not detecting new devices after a while 2025-11-22 16:08:11 +01:00
Leon Böttger 734cf766d5 Add missing App Uses Non-Exempt Encryption flag 2025-11-18 20:42:30 +01:00
Leon Böttger f948c30027 Fix macOS toast view autohide 2025-11-18 20:39:13 +01:00
Leon Böttger 0c440b7511 macOS: Request review only after toast was dismissed 2025-11-18 20:25:18 +01:00
Leon Böttger bf5724ece1 Bump version number 2025-11-18 20:10:02 +01:00
Leon Böttger 658a2ff28e macOS: Add ability to save images/videos directly to photos app 2025-11-18 19:16:07 +01:00
Leon Böttger 4ddf39817c Feat: Remove files that were already imported in photos app 2025-11-16 15:13:47 +01:00
Leon Böttger 3fa1d8621e Improve strings 2025-11-16 15:05:11 +01:00
Leon Böttger 394aaa8093 iOS: Add ability to save photos to gallery 2025-11-16 14:31:17 +01:00
Leon Böttger 5e62f3e107 Improve logging 2025-11-16 10:55:21 +01:00
Leon Böttger 3f65ace2ae Bump version 2025-11-15 15:19:35 +01:00
Leon Böttger 41b6eb8e3a Improve iOS introduction 2025-11-15 15:06:14 +01:00
Leon Böttger 1256b91d4f Refactoring 2025-11-15 14:11:20 +01:00
Leon Böttger 9fd07e9326 Comments 2025-11-14 18:57:29 +01:00
Leon Böttger 70a6b5ff3e Fix: Cannot receive phone numbers 2025-11-13 21:34:06 +01:00
Leon Böttger 9536854fa2 Refactoring 2025-11-10 21:22:46 +01:00
Leon Böttger 39ea48fc80 Refactoring 2025-11-10 20:54:17 +01:00
Leon Böttger 4e73fced74 Refactor UIAlertController Logic 2025-11-10 19:44:15 +01:00
Leon Böttger 21f0f865b6 Bump version 2025-11-08 09:50:14 +01:00
Leon Böttger 026ebc33db Do not show review on GitHub release 2025-11-08 09:49:22 +01:00
Leon Böttger d01e1d7c34 Refactoring 2025-11-06 20:57:46 +01:00
Leon Böttger 14390035a3 Refactoring 2025-11-06 19:17:23 +01:00
Leon Böttger a3d295cd38 UI improvements 2025-11-06 19:03:16 +01:00
Leon Böttger 0ca2a13d66 Improve Toast View UI 2025-11-05 15:50:12 +01:00
Leon Böttger 05321adb6d Fix: progress bar not shown immediately when receiving many files 2025-11-05 15:09:07 +01:00
Leon Böttger 31b93a34a2 Fix incoming transmission capsule shape 2025-11-05 11:16:39 +01:00
Leon Böttger 261f91bc94 Add menu bar menu icons 2025-11-05 10:18:52 +01:00
Leon Böttger 646da767a7 Show average process for multiple incoming connnections 2025-10-19 14:39:25 +02:00
Leon Böttger 9d1e698d9b Refactoring 2025-10-19 14:20:44 +02:00
Leon Böttger a69a75cb07 Refactoring 2025-10-19 14:01:02 +02:00
Leon Böttger 1ac988a4fc Add progress notification when receiving files 2025-10-06 19:42:06 +02:00
Leon Böttger 673d4418f8 Better localizations 2025-10-06 14:09:47 +02:00
Leon Böttger 779144d26a Fix: iOS app was unable to send URLs 2025-10-06 14:09:10 +02:00
Leon Böttger 0fa585652d Update error handling instructions 2025-10-05 14:16:53 +02:00
Leon Böttger 1b1206d0b8 Comments 2025-10-05 14:01:37 +02:00
Leon Böttger 86afa8a206 Fix: Connection not cleaned up after unexpected close be peer 2025-10-05 13:07:10 +02:00
Leon Böttger dda001cb1a Fix: Connection not cleaned up after FIN was sent 2025-10-05 12:17:34 +02:00
Leon Böttger 4ae1663779 Update Strings 2025-09-23 12:38:07 +02:00
Leon Böttger 66acbf25a3 Fix logging 2025-09-23 12:02:43 +02:00
Leon Böttger 93537094e4 iOS: UI improvements 2025-09-21 18:53:53 +02:00
Leon Böttger b6b11c00c0 Add support for iPhone 17 Series 2025-09-21 14:49:04 +02:00
Leon Böttger 92671d8983 Clean up screenshots 2025-09-14 21:21:23 +02:00
Leon Böttger bfc23fde15 Add QR Code View to iOS 2025-09-14 21:02:39 +02:00
Leon Böttger a39a5a324a Fix App Icon for iOS 2025-09-13 16:15:02 +02:00
Leon Böttger fd7dfd959b Add Liquid Glass Icon 2025-09-13 15:13:18 +02:00
Leon Böttger bd4b181a49 Refactoring 2025-09-12 11:48:24 +02:00
Leon Böttger f066a11608 Fix: iOS app advertised to scan even though not connected to Wi-Fi 2025-09-12 11:37:48 +02:00
Leon Böttger 5429126c1e Fix: Only one file would be saved for multiple incoming files with same name 2025-09-12 11:20:28 +02:00
Leon Böttger 4bf8a9052d Disable Liquid Glass for now 2025-09-11 15:48:15 +02:00
Leon Böttger f786af9d12 Improve logging 2025-09-11 15:47:12 +02:00
Leon Böttger 6def7dbc9f Refactoring 2025-09-09 14:35:25 +02:00
Leon Böttger fcc0ce0818 If selecting custom folder, automatically enable "openFinderAfterReceiving" 2025-09-09 14:11:54 +02:00
Leon Böttger fd6978cc90 Add alert when selecting invalid folder for storing files 2025-09-09 13:58:36 +02:00
Leon Böttger 12ef87c4da Simplify Menu Bar Items 2025-09-09 12:49:56 +02:00
Leon Böttger 518fabcd33 Add translations for InfoPlist 2025-09-09 12:33:56 +02:00
Leon Böttger eb6bb8fc4d Add more languages 2025-09-09 12:10:47 +02:00
Leon Böttger d443412697 Fix iOS 2025-09-09 11:40:27 +02:00
Leon Böttger 110b0a4ae7 Add alert if share extension disabled 2025-09-09 11:35:44 +02:00
Leon Böttger 2b272c81c1 Refactoring 2025-09-09 11:14:29 +02:00
Leon Böttger 11afc232ab Refactoring UserDefaults 2025-09-09 11:08:13 +02:00
Leon Böttger 943414db58 Fix iOS compilation 2025-09-05 19:31:29 +02:00
Leon Böttger b135229d30 Fix: Mac showed no connection when connected using Ethernet 2025-09-05 19:25:56 +02:00
Leon Böttger 6126d9428d Merge pull request #3 from leonboe1/ios
Add QuickDrop for iOS
2025-09-05 18:56:26 +02:00
182 changed files with 47605 additions and 5039 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
DerivedData/ DerivedData/
.DerivedData/
.DS_Store .DS_Store
xcuserdata/ xcuserdata/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

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
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.
+126
View File
@@ -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()
} }
} }
@@ -62,6 +78,19 @@ class InboundNearbyConnection: NearbyConnection {
if currentState != .receivingFiles { if currentState != .receivingFiles {
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 {
@@ -112,11 +141,64 @@ class InboundNearbyConnection: NearbyConnection {
} }
switch currentState { switch currentState {
case .sentConnectionResponse: case .sentConnectionResponse:
try processPairedKeyEncryptionFrame(frame) if frame.hasV1, frame.v1.hasPairedKeyEncryption {
try processPairedKeyEncryptionFrame(frame)
return
}
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)
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: case .sentPairedKeyResult:
try processPairedKeyResultFrame(frame) 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: case .receivedPairedKeyResult:
try processIntroductionFrame(frame) 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,19 +223,23 @@ 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
guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") } guard frame.payloadChunk.offset == currentOffset else { throw NearbyError.protocolError("Invalid offset into file \(frame.payloadChunk.offset), expected \(currentOffset)") }
guard currentOffset + Int64(frame.payloadChunk.body.count) <= fileInfo.meta.size else { throw NearbyError.protocolError("Transferred file size exceeds previously specified value") } 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
} }
@@ -373,10 +469,29 @@ class InboundNearbyConnection: NearbyConnection {
pairedEncryption.v1 = Sharing_Nearby_V1Frame() pairedEncryption.v1 = Sharing_Nearby_V1Frame()
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(),
pairedEncryption.v1.pairedKeyEncryption.secretIDHash = Data.randomData(length: 6) let publicKey = IdentityManager.shared.getPublicKey()?.toGenericPublicKey(),
pairedEncryption.v1.pairedKeyEncryption.signedData = Data.randomData(length: 72) 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.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,70 +612,237 @@ 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)
payloadID: file.payloadID, usedDestinations.insert(dest)
destinationURL: dest)
let info = InternalFileInfo(
meta: FileMetadata(name: file.name, size: file.size, mimeType: file.mimeType),
payloadID: file.payloadID,
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 os(macOS)
if textMetadata.type == .url || textMetadata.type == .text { if frame.v1.introduction.hasMirroredNotificationMetadata || frame.v1.introduction.hasPairingMetadata {
isControlTransfer = true
let isClipboardText = textMetadata.type == .text mirroredNotificationMetadata = frame.v1.introduction.hasMirroredNotificationMetadata
? frame.v1.introduction.mirroredNotificationMetadata
let metadata = TransferMetadata(files: [], id: id, pinCode: pinCode, textDescription: textMetadata.textTitle, transferType: isClipboardText ? .text : .url, allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil) : nil
textPayloadID = textMetadata.payloadID pairingMetadata = frame.v1.introduction.hasPairingMetadata
? frame.v1.introduction.pairingMetadata
if isClipboardText { : nil
isPlainTextTransfer = true isPairingSetupRequest = pairingMetadata?.setupRequest == true
let hasPairingConfirmation = pairingMetadata?.hasSetupPinHash == true
let pairingUseCase = pairingMetadata.flatMap { PairingUseCase(protoValue: $0.useCase) }
guard let device = self.remoteDeviceInfo else {
self.rejectTransfer(with: .reject)
return
}
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
} }
checkIfCanProceed(metadata: metadata) // 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
} }
else {
rejectDueToUnsupportedFileType(frame) 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
} }
} else {
rejectDueToUnsupportedFileType(frame) submitUserConsent(accepted: true, trustDevice: false)
return
}
#endif
if let textMetadata = frame.v1.introduction.textMetadata.first {
let isURL = textMetadata.type == .url
textPayloadID = textMetadata.payloadID
let isRemoteCopy = frame.v1.introduction.hasUseCase
&& frame.v1.introduction.useCase == .remoteCopy
if isRemoteCopy && !isURL {
let peerFingerprint = peerCertificate?.secretID.hex
let isClipboardSyncAllowed = isAuthenticated
&& receiverAuthenticationAccepted
&& TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: peerFingerprint)
guard isClipboardSyncAllowed else {
log("[InboundNearbyConnection \(self.id)] Rejecting clipboard sync transfer because the sender is not trusted for clipboard sync.")
self.rejectTransfer(with: .reject, markAsUserRejected: false)
return
}
isClipboardSyncTransfer = true
} else {
isClipboardSyncTransfer = false
}
let metadata = TransferMetadata(
files: [],
id: id,
pinCode: pinCode,
textDescription: textMetadata.textTitle,
transferType: isURL ? .url : .text,
allowsToBeAddedAsTrustedDevice: self.peerCertificate != nil,
isClipboardSync: isClipboardSyncTransfer
)
if !isURL {
isPlainTextTransfer = true
}
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 {
log("[InboundNearbyConnection \(self.id)] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
lastError = NearbyError.canceled(reason: .unsupportedType)
rejectTransfer(with: .unsupportedAttachmentType)
} }
func checkIfCanProceed(metadata: TransferMetadata) { func checkIfCanProceed(metadata: TransferMetadata) {
let acceptAutomatically = isAuthenticated || UserDefaults.standard.bool(forKey: UserDefaultsKeys.automaticallyAcceptFiles.rawValue) let acceptAutomatically = isAuthenticated || Settings.sharedInstance.automaticallyAcceptFiles
DispatchQueue.main.async { DispatchQueue.main.async {
self.delegate?.obtainUserConsent(transfer: metadata, device: self.remoteDeviceInfo!, connection: self, acceptAutomatically: acceptAutomatically) 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 rejectDueToUnsupportedFileType(_ frame: Sharing_Nearby_Frame) {
log("[InboundNearbyConnection \(self.id)] Rejecting transfer due to unsupported file type. Frame is \(frame.debugDescription)")
lastError = NearbyError.canceled(reason: .unsupportedType)
rejectTransfer(with: .unsupportedAttachmentType)
}
func submitUserConsent(accepted: Bool, trustDevice: Bool, storeInTemp: Bool = false) { 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 }
do { try? FileManager.default.removeItem(at: file.destinationURL)
try FileManager.default.removeItem(at: file.destinationURL)
}
catch {
// if it fails, we don't care. Could be because file was not created yet
}
} }
} }
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 {
try file.fileHandle?.close()
} catch {
log("[InboundNearbyConnection \(self.id)] Failed to close recovered file handle for \(file.destinationURL.lastPathComponent): \(error.localizedDescription)")
}
#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)."
}
}
+155 -45
View File
@@ -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,8 +29,13 @@ class NearbyConnection {
var cancelled: Bool = false var cancelled: Bool = false
var isTransferring: Bool = false { var isTransferring: Bool = false {
didSet { didSet {
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.") dataTransferInactivityTimer?.cancel()
startDataTransferTimer(previousBytesTransferred: bytesTransferred) dataTransferInactivityTimer = nil
if isTransferring {
log("[NearbyConnection \(self.id)] Now transferring. Setting up data transfer inactivity timer.")
startDataTransferTimer(previousBytesTransferred: bytesTransferred)
}
} }
} }
@@ -37,7 +43,11 @@ class NearbyConnection {
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.hasPayloadChunk {
if payloadTransfer.controlMessage.event == .payloadCanceled { 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 {
sendKeepAlive(ack: true)
log("[NearbyConnection \(self.id)] Sent keep-alive, \(self.bytesTransferred) bytes (\(gigabytesTransferred) GB) sent") }
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 })
} }
@@ -531,15 +615,23 @@ class NearbyConnection {
offlineFrame.version = .v1 offlineFrame.version = .v1
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")
self.disconnect()
} }
log("[NearbyConnection \(self.id)] Sent disconnection frame during sendDisconnectionAndDisconnect") if encryptionDone {
disconnect() 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()
} }
File diff suppressed because it is too large. Load diff
@@ -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,25 +75,24 @@ 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)
} }
} }
} }
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() {
@@ -147,15 +159,50 @@ class OutboundNearbyConnection: NearbyConnection {
switch currentState { switch currentState {
case .sentPairedKeyEncryption: case .sentPairedKeyEncryption:
try processPairedKeyEncryption(frame: frame) if frame.hasV1, frame.v1.hasPairedKeyEncryption {
try processPairedKeyEncryption(frame: frame)
return
}
if frame.hasV1, frame.v1.hasPairedKeyResult {
log("[OutboundNearbyConnection \(self.id)] Received pairedKeyResult before pairedKeyEncryption; continuing for legacy compatibility.")
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: case .sentPairedKeyResult:
try processPairedKeyResult(frame: frame) 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:
try processConsent(frame: frame) if frame.version == .v1, frame.v1.type == .response {
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()
pairedResult.v1.pairedKeyResult.status = .unable if receiverAuthenticationPolicy.requiresReceiverAuthentication {
try verifyReceiverAuthentication(frame: frame)
pairedResult.v1.pairedKeyResult.status = .success
} else {
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)
@@ -425,8 +494,86 @@ class OutboundNearbyConnection: NearbyConnection {
try sendDisconnectionAndDisconnect() try sendDisconnectionAndDisconnect()
} }
} }
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()
}
}
+245 -20
View File
@@ -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 UTType.pdf.localizedDescription ?? "PDF"
}
if let type, type.conforms(to: .image) {
return UTType.image.localizedDescription ?? "Image"
}
if let type, type.conforms(to: .video) || type.conforms(to: .movie) {
return UTType.video.localizedDescription ?? "Video"
}
if let type, type.conforms(to: .audio) {
return UTType.audio.localizedDescription ?? "Audio"
}
if let type, type.conforms(to: .text) {
return UTType.text.localizedDescription ?? "Text"
}
if let type, type.conforms(to: .archive) {
return UTType.archive.localizedDescription ?? "Archive"
}
if let type, let description = type.localizedDescription {
return description
}
return "File"
}
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
}
} }
} }
@@ -147,17 +350,21 @@ public struct EndpointInfo {
var name: String? var name: String?
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 {
@@ -210,19 +423,30 @@ public struct EndpointInfo {
var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)] var endpointInfo: [UInt8] = [UInt8(deviceType.rawValue << 1)]
// 16 bytes: unknown random bytes // 16 bytes: unknown random bytes
for _ in 0 ... 15 { for _ in 0 ... 15 {
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)
+506 -186
View File
@@ -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)
}
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 !_storage._textMetadata.isEmpty {
} try visitor.visitRepeatedMessageField(value: _storage._textMetadata, fieldNumber: 2)
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
} }
} try { if let v = _storage._requiredPackage {
} try visitor.visitSingularStringField(value: v, fieldNumber: 3)
} }()
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { if !_storage._wifiCredentialsMetadata.isEmpty {
// The use of inline closures is to circumvent an issue where the compiler try visitor.visitRepeatedMessageField(value: _storage._wifiCredentialsMetadata, fieldNumber: 4)
// allocates stack space for every if/case branch local when no optimizations }
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and if !_storage._appMetadata.isEmpty {
// https://github.com/apple/swift-protobuf/issues/1182 try visitor.visitRepeatedMessageField(value: _storage._appMetadata, fieldNumber: 5)
if !self.fileMetadata.isEmpty { }
try visitor.visitRepeatedMessageField(value: self.fileMetadata, fieldNumber: 1) try { if let v = _storage._startTransfer {
} try visitor.visitSingularBoolField(value: v, fieldNumber: 6)
if !self.textMetadata.isEmpty { } }()
try visitor.visitRepeatedMessageField(value: self.textMetadata, fieldNumber: 2) if !_storage._streamMetadata.isEmpty {
} try visitor.visitRepeatedMessageField(value: _storage._streamMetadata, fieldNumber: 7)
try { if let v = self._requiredPackage { }
try visitor.visitSingularStringField(value: v, fieldNumber: 3) try { if let v = _storage._useCase {
} }() try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
if !self.wifiCredentialsMetadata.isEmpty { } }()
try visitor.visitRepeatedMessageField(value: self.wifiCredentialsMetadata, fieldNumber: 4) if !_storage._previewPayloadIds.isEmpty {
} try visitor.visitRepeatedInt64Field(value: _storage._previewPayloadIds, fieldNumber: 9)
if !self.appMetadata.isEmpty { }
try visitor.visitRepeatedMessageField(value: self.appMetadata, fieldNumber: 5) try { if let v = _storage._mirroredNotificationMetadata {
} try visitor.visitSingularMessageField(value: v, fieldNumber: 1001)
try { if let v = self._startTransfer { } }()
try visitor.visitSingularBoolField(value: v, fieldNumber: 6) try { if let v = _storage._pairingMetadata {
} }() try visitor.visitSingularMessageField(value: v, fieldNumber: 1002)
if !self.streamMetadata.isEmpty { } }()
try visitor.visitRepeatedMessageField(value: self.streamMetadata, fieldNumber: 7)
}
try { if let v = self._useCase {
try visitor.visitSingularEnumField(value: v, fieldNumber: 8)
} }()
if !self.previewPayloadIds.isEmpty {
try visitor.visitRepeatedInt64Field(value: self.previewPayloadIds, fieldNumber: 9)
} }
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
+89 -1
View File
@@ -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
}
}
+7 -37
View File
@@ -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)
} }
+36 -5
View File
@@ -10,6 +10,7 @@ import Foundation
import SwiftECC import SwiftECC
import Security import Security
import CryptoKit import CryptoKit
import LUI
class IdentityManager { class IdentityManager {
@@ -50,17 +51,21 @@ class IdentityManager {
private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) { private func generateAndStoreKeyPair() -> (ECPrivateKey, ECPublicKey) {
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
}
} }
-15
View File
@@ -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
}
}
-126
View File
@@ -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
}
}
}
+54
View File
@@ -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
}
}
}
-259
View File
@@ -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)
}
}
+1
View File
@@ -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()
+180 -18
View File
@@ -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,23 +78,95 @@ 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 {
@@ -83,5 +175,75 @@ class TrustStore: ObservableObject {
let device: RemoteDeviceInfo let device: RemoteDeviceInfo
let creationDate: Date let creationDate: Date
let certificateData: Data let certificateData: Data
let grantedUseCases: Set<PairingUseCase>
init(
device: RemoteDeviceInfo,
creationDate: Date,
certificateData: Data,
grantedUseCases: Set<PairingUseCase> = []
) {
self.device = device
self.creationDate = creationDate
self.certificateData = certificateData
self.grantedUseCases = grantedUseCases
}
private enum CodingKeys: String, CodingKey {
case device
case creationDate
case certificateData
case grantedUseCases
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
device = try container.decode(RemoteDeviceInfo.self, forKey: .device)
creationDate = try container.decode(Date.self, forKey: .creationDate)
certificateData = try container.decode(Data.self, forKey: .certificateData)
grantedUseCases = try container.decodeIfPresent(Set<PairingUseCase>.self, forKey: .grantedUseCases) ?? []
}
func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate {
TrustedCertificate(
device: device,
creationDate: creationDate,
certificateData: certificateData,
grantedUseCases: grantedUseCases.union([useCase])
)
}
}
struct PendingPairingTrust: Equatable {
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)
} }
} }
+25 -45
View File
@@ -6,63 +6,43 @@
// //
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,
let finalURL = FileManager.default.temporaryDirectory zipExtension: String = "zip",
fromDirectory directoryURL: URL
) throws -> URL {
guard directoryURL.isDirectory else { throw CreateZipError.urlNotADirectory(directoryURL) }
let zipFinalURL = FileManager.default.temporaryDirectory
.appendingPathComponent(zipFilename) .appendingPathComponent(zipFilename)
.appendingPathExtension(zipExtension) .appendingPathExtension(zipExtension)
return try createAt( if FileManager.default.fileExists(atPath: zipFinalURL.path) {
zipFinalURL: finalURL, try FileManager.default.removeItem(at: zipFinalURL)
fromDirectory: directoryURL }
try FileManager.default.zipItem(
at: directoryURL,
to: zipFinalURL,
shouldKeepParent: true,
compressionMethod: .deflate
) )
}
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) {
try FileManager.default.removeItem(at: zipFinalURL)
}
try FileManager.default.moveItem(at: zipCreatedURL, to: zipFinalURL)
} catch {
fileManagerError = error
}
}
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
} }
} }
+21
View File
@@ -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>
File diff suppressed because it is too large. Load diff
@@ -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" : "c2f49bee454760e433bafff17bfdc5b96f75d7c8",
"version" : "1.21.0" "version" : "1.23.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" : "3af5f2e26d102887980957b2d88a38ea8604b8d8"
} }
}, },
{ {
@@ -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" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f",
"version" : "1.31.0" "version" : "1.38.0"
} }
}, },
{ {
@@ -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" : "c3db5696c625f8571f5f26fb292568db517374cc"
}
},
{
"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"
@@ -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"
@@ -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>
@@ -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
+152 -81
View File
@@ -12,6 +12,7 @@ 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,7 +26,6 @@ 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?
var showsFirewallAlert = false var showsFirewallAlert = false
@@ -35,70 +35,105 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: NSApplicationDelegate functions // MARK: NSApplicationDelegate functions
func applicationDidFinishLaunching(_: Notification) { func applicationDidFinishLaunching(_: Notification) {
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: nil,
keyEquivalent: "" keyEquivalent: ""
) )
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
if !UserDefaults.standard.bool(forKey: UserDefaultsKeys.appLaunchedBefore.rawValue) { NearbyConnectionManager.shared.connectionUpdateCallback = { isConnected in
log("[AppDelegate] Opening Welcome Screen") if isConnected {
// open welcome screen self.statusItem?.button?.image = NSImage(named: "MenuBarIcon")
openWelcomeScreen() self.visibleItem?.title = self.getDefaultVisibleLabel()
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 {
log("[AppDelegate] QuickDrop+ available")
}
} }
else {
BezelNotification.show(messageText: "ReadyToReceive".localized(), icon: .receiveIcon) self.statusItem?.button?.image = NSImage(named: "MenuBarIconSlash")
self.visibleItem?.title = "NoNetworkConnection".localized()
}
}
NearbyConnectionManager.shared.changedDeviceNameCallback = {
self.visibleItem?.title = self.getDefaultVisibleLabel()
}
// 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
} }
UNUserNotificationCenter.current().delegate = self
receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in receiveModel = ReceiveModel(controlPlusScreen: { shouldOpen in
if shouldOpen { if shouldOpen {
self.openPlusScreen() self.openPlusScreen()
@@ -112,25 +147,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,36 +161,65 @@ 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()
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") {
if url.scheme == "quickdrop", url.host == "openLog", let url = LogManager.sharedInstance.logFileURL { log("[AppDelegate] sendLoggingString: in extension, redirecting to main app")
log("Opening log file: \(url)")
// open folder containing the log file if let url = URL(string: "quickdrop://sendLog") {
NSWorkspace.shared.activateFileViewerSelecting([url]) NSWorkspace.shared.open(url)
}
return
}
LogExportPresenter.showUploadLogsAlert()
case "openLog":
if let url = LogManager.sharedInstance.logFileURL {
log("[AppDelegate] Opening log file: \(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
}
} }
} }
} }
@@ -191,13 +243,22 @@ 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() {
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)
@@ -216,9 +277,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) }, openCableTransmissionView: { self.openSheetView(type: .downloadCableConnectionApp) },
checkForNetworkIssues: performDeviceToDeviceCheck checkForNetworkIssues: performDeviceToDeviceCheck
) )
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
@@ -238,25 +299,29 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
welcomeWindow?.makeKeyAndOrderFront(nil) welcomeWindow?.makeKeyAndOrderFront(nil)
welcomeWindow?.level = .normal welcomeWindow?.level = .normal
} }
@objc func openPlusScreen() { @objc func openPlusScreen() {
// If window already exists and is visible, just bring it to the front // If window already exists and is visible, just bring it to the front
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 +350,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 +392,23 @@ 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 getDefaultVisibleLabel() -> String {
let name = NearbyConnectionManager.shared.deviceInfo.name ?? "Unknown".localized()
return String(format: "VisibleAs".localized(), name)
}
} }
struct TransferInfo { struct TransferInfo {
Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

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" : {
Binary file not shown.

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
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

+1 -1
View File
@@ -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"
}, },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

+58
View File
@@ -0,0 +1,58 @@
//
// Configuration.swift
// QuickDrop
//
// Created by Leon Böttger on 15.08.25.
//
import LUI
import SwiftUI
let configuration = LUIConfiguration(
appName: "QuickDrop",
plusVersionInfo: GetPlusViewInformation(
lifetimeID: "plusversion",
monthlyID: "quickdrop.monthly",
yearlyID: "quickdrop.yearly",
plusViewHeader: isMac() ? "plusview_title" : nil,
description: isMac() ? "plusview_description" : "plusview_description_phone",
onBoughtPlusVersionAlertMessage: "plusview_success_description",
plusVersionFeatures: [],
settingsLabelColor: .orange,
settingsFooter: "plusview_footer_phone"
),
hasRoundedFont: false,
switchedToInAppPurchaseWithBundleVersion: AppVersionBundle(appVersion: "1.2", buildVersion: 0, legacyUserDefaultsKey: Settings.UserDefaultsKeys.plusVersionLegacy.rawValue),
copyrightInformation: licenseText,
logUploadDisclaimerMessage: "log_upload_disclaimer_message",
loggingUrl: AppGroup.appGroupDirectory,
introductionViewInformation: "QuickDropWelcomeDescription",
introductionViewImage: Image(.quickDropMockup),
usesAutomaticReviewPromptAndShareSheet: false,
usesPagedIntroduction: true,
supportsSplitView: false,
supportsLiquidGlass: true,
supportsPortraitSplitView: false,
supportsDarkModeShadow: false,
privacyPolicyURL: .privacyPolicyURL
)
extension String {
static let privacyPolicyURL = "https://leonboettger.com/quickdrop-privacy"
}
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff. Show more