diff --git a/NearDrop/Assets.xcassets/Filter.imageset/Contents.json b/NearDrop/Assets.xcassets/Filter.imageset/Contents.json new file mode 100644 index 0000000..14d492f --- /dev/null +++ b/NearDrop/Assets.xcassets/Filter.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Filter.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NearDrop/Assets.xcassets/Filter.imageset/Filter.png b/NearDrop/Assets.xcassets/Filter.imageset/Filter.png new file mode 100644 index 0000000..d3822c6 Binary files /dev/null and b/NearDrop/Assets.xcassets/Filter.imageset/Filter.png differ diff --git a/NearDrop/Assets.xcassets/Firewall.imageset/Contents.json b/NearDrop/Assets.xcassets/Firewall.imageset/Contents.json new file mode 100644 index 0000000..b1febfd --- /dev/null +++ b/NearDrop/Assets.xcassets/Firewall.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Firewall.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NearDrop/Assets.xcassets/Firewall.imageset/Firewall.png b/NearDrop/Assets.xcassets/Firewall.imageset/Firewall.png new file mode 100644 index 0000000..60e7197 Binary files /dev/null and b/NearDrop/Assets.xcassets/Firewall.imageset/Firewall.png differ diff --git a/NearDrop/Assets.xcassets/Router.imageset/Contents.json b/NearDrop/Assets.xcassets/Router.imageset/Contents.json new file mode 100644 index 0000000..08a7f10 --- /dev/null +++ b/NearDrop/Assets.xcassets/Router.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Router.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NearDrop/Assets.xcassets/Router.imageset/Router.png b/NearDrop/Assets.xcassets/Router.imageset/Router.png new file mode 100644 index 0000000..5fd847e Binary files /dev/null and b/NearDrop/Assets.xcassets/Router.imageset/Router.png differ diff --git a/NearDrop/IssueViews/ApIsolationIssueView.swift b/NearDrop/IssueViews/ApIsolationIssueView.swift new file mode 100644 index 0000000..0542dda --- /dev/null +++ b/NearDrop/IssueViews/ApIsolationIssueView.swift @@ -0,0 +1,133 @@ +// +// ApIsolationIssueView.swift +// QuickDrop +// +// Created by Leon Böttger on 15.05.25. +// + +import SwiftUI +import Foundation +import AppKit +import Network + +struct ApIsolationIssueView: View { + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + + IssueView(image: .router, header: "Your router doesn't allow device-to-device communication", description: """ +QuickDrop has detected that your Wi-Fi router might block devices from talking to each other. To transfer files between your phone and your Mac, you’ll need to disable client isolation (also known as AP isolation or Wi-Fi isolation). + +Here’s how to disable it: + +1. Log into your router: Open a web browser and enter your router’s IP address (usually 192.168.0.1 or 192.168.1.1), or use the button on the bottom of this page. Log in with your admin username and password. (Check your router label or manual if you’re unsure.) + +2. Find the setting: Look under sections like Wireless Settings, Advanced Settings, or Security. Look for options named “AP Isolation”, “Client Isolation”, “Wireless Isolation”, or “Intra-BSS Blocking”. + +3. Disable it: Make sure this setting is turned off. Save your changes and reboot the router if necessary. + +Once disabled, devices on your Wi-Fi (like your phone and Mac) will be able to discover and connect to each other. +""", actionLabel: "Open Router Settings") { + openRouterSettingsPage() + } + } +} + + +func openRouterSettingsPage() { + guard let routerIP = getDefaultGatewayIP() else { + + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .critical + + alert.messageText = "Could not find router IP" + alert.informativeText = "QuickDrop was not able to determine the address of your router. Please refer to the user manual of your Wi-Fi router to obtain its address." + + alert.addButton(withTitle: "CloseAlert".localized()) + + let _ = alert.runModal() + } + + return + } + + if let url = URL(string: "http://\(routerIP)") { + NSWorkspace.shared.open(url) + } +} +/// Attempts to get the default gateway IP address (e.g., 192.168.1.1) +/// and ensures it's reachable. Returns nil if unreachable or not found. +func getDefaultGatewayIP() -> String? { + let output = shell("netstat -rn | grep default") + let lines = output.components(separatedBy: .newlines) + + for line in lines { + let components = line.split(separator: " ").map(String.init).filter { !$0.isEmpty } + if components.count >= 2 { + let gateway = components[1] + if isHostReachable(gateway) { + return gateway + } else { + print("Gateway \(gateway) not reachable.") + return nil + } + } + } + return nil +} + +/// Executes a shell command and returns the output +@discardableResult +func shell(_ command: String) -> String { + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.arguments = ["-c", command] + task.launchPath = "/bin/zsh" + task.launch() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + return String(data: data, encoding: .utf8) ?? "" +} + +/// Checks if a host (IP address or hostname) is reachable on port 80 +func isHostReachable(_ host: String) -> Bool { + let semaphore = DispatchSemaphore(value: 0) + var reachable = false + + let hostEndpoint = NWEndpoint.Host(host) + let port: NWEndpoint.Port = 80 + + let connection = NWConnection(host: hostEndpoint, port: port, using: .tcp) + + connection.stateUpdateHandler = { state in + switch state { + case .ready: + reachable = true + connection.cancel() + semaphore.signal() + case .failed(_), .cancelled: + connection.cancel() + semaphore.signal() + default: + break + } + } + + connection.start(queue: .global()) + _ = semaphore.wait(timeout: .now() + 3) // wait max 3 seconds + + return reachable +} + + + +#Preview { + ApIsolationIssueView() + .frame(width: 600, height: 400) +} + diff --git a/NearDrop/IssueViews/FirewallIssueView.swift b/NearDrop/IssueViews/FirewallIssueView.swift new file mode 100644 index 0000000..f1e7dfd --- /dev/null +++ b/NearDrop/IssueViews/FirewallIssueView.swift @@ -0,0 +1,38 @@ +// +// FirewallIssueView.swift +// QuickDrop +// +// Created by Leon Böttger on 15.05.25. +// + +import SwiftUI +import Foundation + +struct FirewallIssueView: View { + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + + IssueView(image: .firewall, header: "A firewall blocks incoming file transfers", description: """ +QuickDrop has detected that a firewall prevents that your Android device can connect to your Mac. Firewalls are used to block incoming connections per default. For receiving files, it is required that incoming connections are allowed. This is because your phone tries to connect to your Mac – creating an incoming connection. + +You most likely use an antivirus program on your Mac. You have two options: + +1. Allow incoming connections for QuickDrop in your firewall settings. This procedure varies with every program, so you may have to ask the manufacturer of your firewall/antivirus software how this can be done. + +2. Disable the firewall of your antivirus software, and use the Mac's inbuilt firewall instead. Apple's firewall is optimized for QuickDrop. + +Here’s how to disable the firewall of your antivirus program: Open System Settings on your Mac, and navigate to Network > VPN & Filter and disable the content filter. Then, navigate to Network > Firewall and click the toggle to enable the Mac's firewall. + +Once this is done, your phone will be able to connect to your Mac. +""") + } +} + + +#Preview { + FirewallIssueView() + .frame(width: 600, height: 400) +} + diff --git a/NearDrop/IssueViews/IssueView.swift b/NearDrop/IssueViews/IssueView.swift new file mode 100644 index 0000000..913feab --- /dev/null +++ b/NearDrop/IssueViews/IssueView.swift @@ -0,0 +1,79 @@ +// +// IssueView.swift +// QuickDrop +// +// Created by Leon Böttger on 15.05.25. +// + +import SwiftUI +import Foundation + +struct IssueView: View { + + @Environment(\.colorScheme) var colorScheme + + let image: ImageResource + let header: String + let description: String + + var actionLabel: String? = nil + var action: (() -> Void)? = nil + + var body: some View { + + let imageSize = 120.0 + ScrollView { + + ZStack { + + if colorScheme == .light { + Color.gray.opacity(0.1) + } + else { + Color.black.opacity(0.2) + } + + Image(image) + .resizable() + .frame(width: imageSize, height: imageSize) + .overlay( + Image(systemName: "exclamationmark.circle.fill") + .font(.system(size: imageSize * 0.2)) + .background(Circle().foregroundColor(.white)) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + .foregroundColor(.red) + + .padding(imageSize * 0.09) + .padding(.trailing, image == .filter ? 15 : 0) + .padding(.bottom, imageSize * 0.03) + ) + .padding(.vertical) + + } + .padding(.bottom, 20) + + Text(header) + .font(.title2) + .fontWeight(.bold) + + Text(description) + .fixedSize(horizontal: false, vertical: true) + .opacity(0.7) + .padding() + + if let actionLabel = actionLabel, let action = action { + Button(actionLabel) { + action() + } + .padding(.bottom, 30) + } + } + } +} + + +#Preview { + NetworkFilterIssueView() + .frame(width: 600, height: 400) +} + diff --git a/NearDrop/IssueViews/NetworkFilterIssueView.swift b/NearDrop/IssueViews/NetworkFilterIssueView.swift new file mode 100644 index 0000000..327be25 --- /dev/null +++ b/NearDrop/IssueViews/NetworkFilterIssueView.swift @@ -0,0 +1,32 @@ +// +// NetworkFilterIssueView.swift +// QuickDrop +// +// Created by Leon Böttger on 15.05.25. +// + +import SwiftUI +import Foundation + +struct NetworkFilterIssueView: View { + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + + IssueView(image: .filter, header: "A packet filter prevents file transfer", description: """ +QuickDrop has detected that a program installed on your Mac modifies network traffic. As a result of that, the file you tried to send from your Android device could not be decrypted. + +Your Mac may have a faulty packet filter enabled, which might be installed by your antivirus program (e.g. ESET). Open System Preferences > Network > VPN & Filter and disable the content filter. + +Once this is done, you should be able to transfer files successfully. +""") + } +} + + +#Preview { + NetworkFilterIssueView() + .frame(width: 600, height: 400) +} + diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 2b9294d..bbacf97 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -231,6 +231,7 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SelfTest; sourceTree = ""; }; + EB6E743F2DD5CBFB00331466 /* IssueViews */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IssueViews; sourceTree = ""; }; EB7CD13B2D6F8B6500CB0CB5 /* IAP */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = IAP; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -322,6 +323,7 @@ EB7F54132D78E9E400989564 /* LicenseWindow.swift */, EB6F62D62D91F4AC00534EC2 /* SettingsView.swift */, EB36435F2D73A29F009CDFF8 /* AudioManager.swift */, + EB6E743F2DD5CBFB00331466 /* IssueViews */, EB96ED8E2D7F8103004B3FF1 /* Localized.swift */, EB7F540F2D78E6DD00989564 /* Sidebar.swift */, EB7CD13B2D6F8B6500CB0CB5 /* IAP */, @@ -428,6 +430,7 @@ ); fileSystemSynchronizedGroups = ( EB1A7E422DD4D8BD00A6D5A3 /* SelfTest */, + EB6E743F2DD5CBFB00331466 /* IssueViews */, EB7CD13B2D6F8B6500CB0CB5 /* IAP */, ); name = QuickDrop;