From e6d1c7b1a3fc9a2eb857634ba7e7d647272738a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leon=20B=C3=B6ttger?= <49094811+leonboe1@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:29:35 +0200 Subject: [PATCH] Add device name lookup for unnamed devices --- .../Connection/NearbyConnectionManager.swift | 64 ++++- .../Connection/NearbyDeviceNameResolver.swift | 244 ++++++++++++++++++ NearbyShare/Model/DataModel.swift | 36 ++- QuickDrop.xcodeproj/project.pbxproj | 14 + QuickDropiOS/View/ContentView.swift | 18 +- ShareExtension/ShareViewController.swift | 11 +- .../View/Base.lproj/ShareViewController.xib | 12 +- ShareExtension/View/DeviceListCell.swift | 11 + ShareExtension/View/DeviceListCell.xib | 11 + 9 files changed, 397 insertions(+), 24 deletions(-) create mode 100644 NearbyShare/Connection/NearbyDeviceNameResolver.swift diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index b7fa366..e5e6c58 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -36,6 +36,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private var startedDeviceDiscovery = false private var startedAdvertising = false { didSet { informAboutStatus() }} private var browsers: [NWBrowser] = [] + private let deviceNameResolver = NearbyDeviceNameResolver() private let serviceTypes = ["_FC9F5ED42C8A._tcp."] private var qrCodePrivateKey: ECPrivateKey? private var qrCodeAdvertisingToken: Data? @@ -315,11 +316,11 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb var txtRecord: [String: Data] = [ txtKeyEndpointInfo: deviceInfo.serialize().urlSafeBase64EncodedString().data(using: .utf8)!, ] - #if os(macOS) - txtRecord[txtQuickDropCapabilities] = Data(notificationSyncCapabilityToken.utf8) if let keyFingerprint = notificationSyncKeyFingerprintHex() { txtRecord[txtQuickDropDeviceKeyFingerprint] = Data(keyFingerprint.utf8) } + #if os(macOS) + txtRecord[txtQuickDropCapabilities] = Data(notificationSyncCapabilityToken.utf8) #endif service.setTXTRecord(NetService.data(fromTXTRecord: txtRecord)) service.publish() @@ -473,7 +474,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb browsers.removeAll() startedDeviceDiscovery = false - + deviceNameResolver.cancelAll() for delegate in outboundAppDelegates { @@ -552,14 +553,40 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb guard case let NWBrowser.Result.Metadata.bonjour(txtRecord) = service.metadata else { return } guard let endpointInfoEncoded = txtRecord.dictionary[txtKeyEndpointInfo] else { return } guard let endpointInfoData = Data.dataFromUrlSafeBase64(endpointInfoEncoded) else { return } - var endpointInfo = EndpointInfo(data: endpointInfoData) + let parsedEndpointInfo = EndpointInfo(data: endpointInfoData) + var endpointInfo = parsedEndpointInfo ?? EndpointInfo(name: unnamedAndroidFallbackDeviceName, deviceType: .unknown) - - let cleanedName = endpointInfo.name?.trimmingCharacters(in: .whitespacesAndNewlines) - if cleanedName?.isEmpty != false { + let isQuickDropPeer = txtRecord.dictionary[txtQuickDropDeviceKeyFingerprint] != nil + let needsDeviceNameLookup = deviceNameResolver.needsLookup(for: parsedEndpointInfo) + if needsDeviceNameLookup { endpointInfo.name = unnamedAndroidFallbackDeviceName } - let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) + let deviceInfo = addFoundDevice( + foundService: &foundService, + endpointInfo: endpointInfo, + endpointID: endpointID, + isQuickDropPeer: isQuickDropPeer + ) + + if needsDeviceNameLookup { + deviceNameResolver.resolveName(for: service, endpointID: endpointID) { [weak self] deviceName in + guard let self else { return } + guard self.foundServices[endpointID]?.device?.name == self.unnamedAndroidFallbackDeviceName else { return } + + var resolvedEndpointInfo = endpointInfo + resolvedEndpointInfo.name = deviceName + var resolvedFoundService = FoundServiceInfo(service: service) + _ = self.addFoundDevice( + foundService: &resolvedFoundService, + endpointInfo: resolvedEndpointInfo, + endpointID: endpointID, + isQuickDropPeer: isQuickDropPeer + ) + } + } + else { + deviceNameResolver.cancelLookup(for: endpointID) + } if let qrData = endpointInfo.qrCodeData, let qrCodeAdvertisingToken = qrCodeAdvertisingToken, let qrCodeNameEncryptionKey = qrCodeNameEncryptionKey { @@ -576,7 +603,12 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb let decryptedName = try AES.GCM.open(box, using: qrCodeNameEncryptionKey, authenticating: qrCodeAdvertisingToken) guard let name = String.init(data: decryptedName, encoding: .utf8) else { return } endpointInfo.name = name - let deviceInfo = addFoundDevice(foundService: &foundService, endpointInfo: endpointInfo, endpointID: endpointID) + let deviceInfo = addFoundDevice( + foundService: &foundService, + endpointInfo: endpointInfo, + endpointID: endpointID, + isQuickDropPeer: isQuickDropPeer + ) for delegate in outboundAppDelegates { delegate.startTransferWithQrCode(device: deviceInfo) } @@ -588,9 +620,18 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb } - private func addFoundDevice(foundService: inout FoundServiceInfo, endpointInfo: EndpointInfo, endpointID: String) -> RemoteDeviceInfo { + private func addFoundDevice( + foundService: inout FoundServiceInfo, + endpointInfo: EndpointInfo, + endpointID: String, + isQuickDropPeer: Bool = false + ) -> RemoteDeviceInfo { let hadPreviousDevice = foundServices[endpointID]?.device != nil - let deviceInfo = RemoteDeviceInfo(info: endpointInfo, id: endpointID) + let deviceInfo = RemoteDeviceInfo( + info: endpointInfo, + id: endpointID, + isQuickDropPeer: isQuickDropPeer + ) foundService.device = deviceInfo foundServices[endpointID] = foundService @@ -610,6 +651,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb private func removeFoundDevice(service: NWBrowser.Result) { guard let endpointID = endpointID(for: service) else { return } + deviceNameResolver.cancelLookup(for: endpointID) guard let _ = foundServices.removeValue(forKey: endpointID) else { return } for delegate in outboundAppDelegates { delegate.removeDevice(id: endpointID) diff --git a/NearbyShare/Connection/NearbyDeviceNameResolver.swift b/NearbyShare/Connection/NearbyDeviceNameResolver.swift new file mode 100644 index 0000000..fe03f62 --- /dev/null +++ b/NearbyShare/Connection/NearbyDeviceNameResolver.swift @@ -0,0 +1,244 @@ +// +// NearbyDeviceNameResolver.swift +// QuickDrop +// +// Created by Leon Böttger on 25.04.2026. +// + +import Darwin +import Foundation +import Network + +final class NearbyDeviceNameResolver { + private var hostnameResolvers: [UUID: NearbyNetworkScannerHostnameResolver] = [:] + 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 = NearbyNetworkScannerHostnameResolver(result: service, resolveTimeout: 0.5, completion: { [weak self] hostname in + self?.hostnameResolvers.removeValue(forKey: resolverID) + completion(hostname) + }) else { + completion(nil) + return + } + + hostnameResolvers[resolverID] = resolver + resolver.start() + } +} + + +private final class NearbyNetworkScannerHostnameResolver: NSObject, NetServiceDelegate { + private static let hostnameLookupQueue = DispatchQueue( + label: "com.leonboettger.quickdrop.networkScannerHostname", + qos: .utility + ) + + private let service: NetService + private let resolveTimeout: TimeInterval + private var timeoutWorkItem: DispatchWorkItem? + private var completion: ((String?) -> Void)? + private var finished = false + + init?( + result: NWBrowser.Result, + resolveTimeout: TimeInterval, + completion: @escaping (String?) -> Void + ) { + guard case let NWEndpoint.service(name: name, type: type, domain: domain, interface: _) = result.endpoint else { + return nil + } + + self.service = NetService( + domain: Self.normalizedServiceDomain(domain), + type: Self.normalizedServiceType(type), + name: name + ) + self.resolveTimeout = resolveTimeout + self.completion = completion + super.init() + } + + + func start() { + service.delegate = self + service.resolve(withTimeout: resolveTimeout) + + let timeoutWorkItem = DispatchWorkItem { [weak self] in + self?.finish(hostname: nil) + } + self.timeoutWorkItem = timeoutWorkItem + DispatchQueue.main.asyncAfter(deadline: .now() + resolveTimeout + 1.0, execute: timeoutWorkItem) + } + + + func cancel() { + finish(hostname: nil, notify: false) + } + + + func netServiceDidResolveAddress(_ sender: NetService) { + let numericAddresses = sender.addresses?.compactMap(Self.numericHostString) ?? [] + guard let ipv4Address = numericAddresses.first(where: NearbyNetworkScannerHostLookup.isIPv4Address) else { + finish(hostname: nil) + return + } + + Self.hostnameLookupQueue.async { [weak self] in + let hostname = NearbyNetworkScannerHostLookup.hostname(forIPv4: ipv4Address) + DispatchQueue.main.async { + self?.finish(hostname: hostname) + } + } + } + + + func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { + finish(hostname: nil) + } + + + private func finish(hostname: String?, notify: Bool = true) { + guard !finished else { return } + finished = true + timeoutWorkItem?.cancel() + service.stop() + service.delegate = nil + + let completion = completion + self.completion = nil + + if notify { + completion?(hostname) + } + } + + + 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)." + } + + + private static func numericHostString(from addressData: Data) -> String? { + addressData.withUnsafeBytes { rawBuffer in + guard let baseAddress = rawBuffer.baseAddress else { return nil } + + var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let sockaddrPointer = baseAddress.assumingMemoryBound(to: sockaddr.self) + let result = getnameinfo( + sockaddrPointer, + socklen_t(addressData.count), + &hostBuffer, + socklen_t(hostBuffer.count), + nil, + 0, + NI_NUMERICHOST + ) + + guard result == 0 else { return nil } + return String(cString: hostBuffer) + .split(separator: "%") + .first + .map(String.init) + } + } +} + + +private enum NearbyNetworkScannerHostLookup { + static func hostname(forIPv4 address: String) -> String? { + reverseDNSName(forIPv4: address).flatMap(prettifyDisplayName) + } + + + static func isIPv4Address(_ address: String) -> Bool { + !address.contains(":") + } + + + private static func reverseDNSName(forIPv4 address: String) -> String? { + var socketAddress = sockaddr_in() + socketAddress.sin_len = UInt8(MemoryLayout.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.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 + } +} diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index 02daec3..48a25ff 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -62,20 +62,52 @@ public struct RemoteDeviceInfo: Codable, Identifiable, Equatable { public let name: String? public let type: DeviceType public let qrCodeData: Data? + public let isQuickDropPeer: Bool public var id: String? - init(name: String?, type: DeviceType?, id: String? = nil) { + init(name: String?, type: DeviceType?, id: String? = nil, isQuickDropPeer: Bool = false) { self.name = name self.type = type ?? .phone self.id = id self.qrCodeData = nil + self.isQuickDropPeer = isQuickDropPeer } - init(info: EndpointInfo, id: String? = nil) { + init(info: EndpointInfo, id: String? = nil, isQuickDropPeer: Bool = false) { self.name = info.name self.type = info.deviceType self.qrCodeData = info.qrCodeData self.id = id + self.isQuickDropPeer = isQuickDropPeer + } + + private enum CodingKeys: String, CodingKey { + case name + case type + case qrCodeData + case isQuickDropPeer + case isVerifiedQuickDropPeer + case id + } + + 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) + } + + 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) } diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index e42a004..a69f471 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB979F8D2E50CA23008B8C5C /* Configuration.swift */; }; EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB950B762E521712002D68FA /* AttachmentDetails.swift */; }; EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; }; @@ -147,6 +148,7 @@ EB3B9F6C2EF7151900D6704E /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2F9BFA2DA04B67004010A2 /* Helpers.swift */; }; EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E92E7025D900FD749C /* ApIsolationTester.swift */; }; EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */; }; @@ -199,11 +201,13 @@ EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB5C37D02E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */; }; EB5C37D12E377A7F0052EFD7 /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; EB5C37D22E377A7F0052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */; }; EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB5C37D62E377ACF0052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D52E377ACF0052EFD7 /* SwiftProtobuf */; }; EB5C37D82E377ACF0052EFD7 /* SwiftECC in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D72E377ACF0052EFD7 /* SwiftECC */; }; EB5C37DA2E377AD60052EFD7 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = EB5C37D92E377AD60052EFD7 /* SwiftProtobuf */; }; @@ -213,6 +217,7 @@ EB5C37F12E377CE70052EFD7 /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; EB5C37F22E377CE70052EFD7 /* NearbyConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 698DFAE529E2F91A0064F247 /* NearbyConnection.swift */; }; EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2A29E18CB500A442DA /* device_to_device_messages.pb.swift */; }; EB5C37F82E377CE70052EFD7 /* SleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB2B41E32DC63AC80070FC39 /* SleepManager.swift */; }; @@ -285,6 +290,7 @@ EB950B502E520436002D68FA /* wire_format.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */; }; EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2D29E18CB500A442DA /* securemessage.pb.swift */; }; EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */; }; + EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */; }; EB950B532E520436002D68FA /* ukey.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69DA9A2829E18CB500A442DA /* ukey.pb.swift */; }; EB950B542E520436002D68FA /* HKDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB886E512E4E19390052B7D9 /* HKDF.swift */; }; EB950B562E520436002D68FA /* DataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB5C36E82E3546100052EFD7 /* DataModel.swift */; }; @@ -495,6 +501,7 @@ 69DA9A1329E0BF5200A442DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 69DA9A1829E0BF5200A442DA /* QuickDrop.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QuickDrop.entitlements; sourceTree = ""; }; 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyConnectionManager.swift; sourceTree = ""; }; + EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearbyDeviceNameResolver.swift; sourceTree = ""; }; 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboundNearbyConnection.swift; sourceTree = ""; }; 69DA9A2829E18CB500A442DA /* ukey.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ukey.pb.swift; sourceTree = ""; }; 69DA9A2929E18CB500A442DA /* wire_format.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = wire_format.pb.swift; sourceTree = ""; }; @@ -788,6 +795,7 @@ isa = PBXGroup; children = ( 69DA9A1E29E0C0B300A442DA /* NearbyConnectionManager.swift */, + EB3B9F192EF7150400D67050 /* NearbyDeviceNameResolver.swift */, 69DA9A2229E17F0400A442DA /* InboundNearbyConnection.swift */, A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */, 691F53BD2ABF03820089FD92 /* OutboundNearbyConnection.swift */, @@ -1315,6 +1323,7 @@ EB5C37D32E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, A1B2C3D42F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, EB5C37D42E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, + EB5C37D42E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */, EB5C37BD2E377A700052EFD7 /* Helpers.swift in Sources */, EB1325EB2E7025D900FD749C /* ApIsolationTester.swift in Sources */, EB1325EC2E7025D900FD749C /* ActiveNetworkInterface.swift in Sources */, @@ -1354,6 +1363,7 @@ EB5C37CE2E377A7F0052EFD7 /* InboundNearbyConnection.swift in Sources */, A1B2C3D52F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, EB5C37CF2E377A7F0052EFD7 /* NearbyConnectionManager.swift in Sources */, + EB5C37CF2E377A7F0052EFD8 /* NearbyDeviceNameResolver.swift in Sources */, EBE5FF422EDB6F6F0019B380 /* Configuration.swift in Sources */, EB7654A12E53252D006B4AA3 /* AttachmentDetails.swift in Sources */, EB886E522E4E19390052B7D9 /* HKDF.swift in Sources */, @@ -1411,6 +1421,7 @@ EB3B9F172EF7150400D6704E /* InboundNearbyConnection.swift in Sources */, A1B2C3D62F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, EB3B9F182EF7150400D6704E /* NearbyConnectionManager.swift in Sources */, + EB3B9F192EF7150400D6704F /* NearbyDeviceNameResolver.swift in Sources */, EB3B9F192EF7150400D6704E /* Configuration.swift in Sources */, EB3B9F1A2EF7150400D6704E /* AttachmentDetails.swift in Sources */, EB3B9F1B2EF7150400D6704E /* HKDF.swift in Sources */, @@ -1476,6 +1487,7 @@ EB3B9F6D2EF7151900D6704E /* InboundNearbyConnection.swift in Sources */, A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */, EB3B9F6E2EF7151900D6704E /* NearbyConnectionManager.swift in Sources */, + EB3B9F6F2EF7151900D6704F /* NearbyDeviceNameResolver.swift in Sources */, EB3B9F6F2EF7151900D6704E /* Helpers.swift in Sources */, EB3B9F702EF7151900D6704E /* ApIsolationTester.swift in Sources */, EB3B9F712EF7151900D6704E /* ActiveNetworkInterface.swift in Sources */, @@ -1496,6 +1508,7 @@ EB16B6142E58B4B9007D9792 /* IdentityManager.swift in Sources */, EB886E672E4F2F800052B7D9 /* License.swift in Sources */, EB5C37F42E377CE70052EFD7 /* NearbyConnectionManager.swift in Sources */, + EB5C37F42E377CE70052EFD8 /* NearbyDeviceNameResolver.swift in Sources */, EB5C37F52E377CE70052EFD7 /* securemessage.pb.swift in Sources */, EB5C37F62E377CE70052EFD7 /* device_to_device_messages.pb.swift in Sources */, EB886E532E4E19390052B7D9 /* HKDF.swift in Sources */, @@ -1551,6 +1564,7 @@ EB950B512E520436002D68FA /* securemessage.pb.swift in Sources */, EBA1BB1F2EF1F6030068E46E /* ChangeDeviceNameModifier.swift in Sources */, EB950B522E520436002D68FA /* NearbyConnectionManager.swift in Sources */, + EB950B522E520436002D68FB /* NearbyDeviceNameResolver.swift in Sources */, EB950B532E520436002D68FA /* ukey.pb.swift in Sources */, EBBC93B22EC8B76600F541A1 /* LocalNetworkPermissionView.swift in Sources */, EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */, diff --git a/QuickDropiOS/View/ContentView.swift b/QuickDropiOS/View/ContentView.swift index f1377a4..126941d 100644 --- a/QuickDropiOS/View/ContentView.swift +++ b/QuickDropiOS/View/ContentView.swift @@ -289,9 +289,21 @@ struct DeviceButtonLabel: View { } VStack(alignment: .leading, spacing: 5) { - Text(name) - .fontWeight(.medium) - .foregroundColor(Color.primary) + HStack(spacing: 5) { + Text(name) + .fontWeight(.medium) + .foregroundColor(Color.primary) + + if device.isQuickDropPeer { + Image(.menuBarIcon) + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 14, height: 14) + .foregroundColor(Color.primary) + .opacity(0.5) + } + } Text(isSelected ? progress ?? "..." : "Available".localized()) .font(.system(size: 12)) diff --git a/ShareExtension/ShareViewController.swift b/ShareExtension/ShareViewController.swift index b885aa5..496be44 100644 --- a/ShareExtension/ShareViewController.swift +++ b/ShareExtension/ShareViewController.swift @@ -95,6 +95,9 @@ class ShareViewController: NSViewController, OutboundAppDelegate { progressDeviceIconWrap!.wantsLayer = true progressDeviceIconWrap!.layer!.masksToBounds = false + progressDeviceSecondaryIcon?.image = NSImage(named: "MenuBarIcon") + progressDeviceSecondaryIcon?.image?.isTemplate = true + progressDeviceSecondaryIcon?.contentTintColor = .white } @@ -250,6 +253,7 @@ class ShareViewController: NSViewController, OutboundAppDelegate { progressView?.animator().isHidden = false progressDeviceName?.stringValue = getDeviceName(device: device) progressDeviceIcon?.image = imageForDeviceType(type: device.type) + progressDeviceSecondaryIcon?.isHidden = !device.isQuickDropPeer progressProgressBar?.startAnimation(nil) progressState?.stringValue = "Preparing".localized() chosenDevice = device @@ -383,8 +387,11 @@ extension ShareViewController: NSCollectionViewDataSource { let device = foundDevices[indexPath[1]] - collectionViewItem.textField?.stringValue = getDeviceName(device: device) - collectionViewItem.imageView?.image = imageForDeviceType(type: device.type) + collectionViewItem.configure( + deviceName: getDeviceName(device: device), + deviceImage: imageForDeviceType(type: device.type), + showsQuickDropPeerMarker: device.isQuickDropPeer && device.type == .phone + ) collectionViewItem.clickHandler = { self.selectDevice(device: device) } diff --git a/ShareExtension/View/Base.lproj/ShareViewController.xib b/ShareExtension/View/Base.lproj/ShareViewController.xib index 2bd3985..393b84a 100644 --- a/ShareExtension/View/Base.lproj/ShareViewController.xib +++ b/ShareExtension/View/Base.lproj/ShareViewController.xib @@ -177,17 +177,17 @@ Gw - - + + diff --git a/ShareExtension/View/DeviceListCell.swift b/ShareExtension/View/DeviceListCell.swift index 06d0296..3ba52e5 100644 --- a/ShareExtension/View/DeviceListCell.swift +++ b/ShareExtension/View/DeviceListCell.swift @@ -9,6 +9,7 @@ import Cocoa class DeviceListCell: NSCollectionViewItem { public var clickHandler: (() -> Void)? + @IBOutlet private var peerMarkerView: NSImageView? override func viewDidLoad() { super.viewDidLoad() @@ -17,10 +18,20 @@ class DeviceListCell: NSCollectionViewItem { btn.setButtonType(.momentaryPushIn) btn.action = #selector(onClick) btn.target = self + + peerMarkerView?.image = NSImage(named: "MenuBarIcon") + peerMarkerView?.image?.isTemplate = true + peerMarkerView?.contentTintColor = .white } @IBAction func onClick(_: Any?) { guard let handler = clickHandler else { return } handler() } + + func configure(deviceName: String, deviceImage: NSImage, showsQuickDropPeerMarker: Bool) { + textField?.stringValue = deviceName + imageView?.image = deviceImage + peerMarkerView?.isHidden = !showsQuickDropPeerMarker + } } diff --git a/ShareExtension/View/DeviceListCell.xib b/ShareExtension/View/DeviceListCell.xib index b43ca8f..44dbbfd 100644 --- a/ShareExtension/View/DeviceListCell.xib +++ b/ShareExtension/View/DeviceListCell.xib @@ -36,10 +36,20 @@ + + + @@ -53,6 +63,7 @@ +