Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@ AGENTS.md
.rambles
.tend-stack
docs/plans/
build.log
build.log
74 changes: 74 additions & 0 deletions airsync-mac/Core/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,86 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// Register Services Provider
NSApp.servicesProvider = self
NSUpdateDynamicServices()

// Register for System Sleep/Wake Notifications
registerForSleepWakeNotifications()
}

private func setupSentry() {
SentryInitializer.start()
}

private func registerForSleepWakeNotifications() {
let nc = NSWorkspace.shared.notificationCenter
nc.addObserver(
self,
selector: #selector(handleSystemSleep),
name: NSWorkspace.willSleepNotification,
object: nil
)
nc.addObserver(
self,
selector: #selector(handleSystemWake),
name: NSWorkspace.didWakeNotification,
object: nil
)
}

@objc private func handleSystemSleep() {
print("[AppDelegate] System going to sleep. Cleaning up connections and background tasks.")

// 1. Mark system as sleeping to block automatic background connections/scans
AppState.shared.isSystemSleeping = true

// 2. Disconnect active device connection cleanly
AppState.shared.disconnectDevice(isManual: false)

// 3. Stop WebSocket server
WebSocketServer.shared.stop()

// 4. Stop BLE scanning explicitly
BLECentralManager.shared.stopScanning()

// 5. Stop UDP Discovery
UDPDiscoveryManager.shared.stop()
}

@objc private func handleSystemWake() {
print("[AppDelegate] System waking up. Resuming services.")

// 1. Mark system as awake
AppState.shared.isSystemSleeping = false

// 2. Restart WebSocket server
startWebSocketServer()

// 3. Restart UDP Discovery
UDPDiscoveryManager.shared.start()

// 4. If BLE Auto Connect is enabled, start BLE scanning
if AppState.shared.isBLEAutoConnectEnabled {
BLECentralManager.shared.isManuallyDisconnected = false
BLECentralManager.shared.startScanning()
}

// 5. Auto connect to known/last connected device via Quick Connect
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
QuickConnectManager.shared.wakeUpLastConnectedDevice()
}
}

private func startWebSocketServer() {
let rawPortInt = AppState.shared.myDevice?.port ?? Int(Defaults.serverPort)
let chosenPort: UInt16
if rawPortInt <= 0 || rawPortInt > 65_535 {
print("[AppDelegate] Invalid configured port \(rawPortInt). Falling back to 8080.")
chosenPort = UInt16(8080)
} else {
chosenPort = UInt16(rawPortInt)
}
WebSocketServer.shared.start(port: chosenPort)
}

func application(_ application: NSApplication, open urls: [URL]) {
if !urls.isEmpty {
QuickShareManager.shared.transferURLs = urls
Expand Down
83 changes: 64 additions & 19 deletions airsync-mac/Core/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class AppState: ObservableObject {
self.isPlus = isPlusLoaded

let adbPortValue = UserDefaults.standard.integer(forKey: "adbPort")
self.adbPort = adbPortValue == 0 ? 5555 : UInt16(adbPortValue)
self.adbPort = (adbPortValue > 0 && adbPortValue <= 65535) ? UInt16(adbPortValue) : 5555
self.adbConnectedIP = UserDefaults.standard.string(forKey: "adbConnectedIP") ?? ""
self.mirroringPlus = UserDefaults.standard.bool(forKey: "mirroringPlus")
self.adbEnabled = UserDefaults.standard.bool(forKey: "adbEnabled")
Expand Down Expand Up @@ -126,7 +126,9 @@ class AppState: ObservableObject {
self.isBLEAutoConnectEnabled = UserDefaults.standard.object(forKey: "isBLEAutoConnectEnabled") == nil ? true : UserDefaults.standard.bool(forKey: "isBLEAutoConnectEnabled")

if isBLEEnabled {
BLECentralManager.shared.startScanning()
DispatchQueue.main.async {
BLECentralManager.shared.startScanning()
}
}

BLECentralManager.shared.$connectionStatus
Expand Down Expand Up @@ -174,6 +176,8 @@ class AppState: ObservableObject {
}

@Published var minAndroidVersion = Bundle.main.infoDictionary?["AndroidVersion"] as? String ?? "2.0.0"
@Published var isManuallyDisconnected: Bool = false
@Published var isSystemSleeping: Bool = false

@Published var device: Device? = nil {
didSet {
Expand Down Expand Up @@ -205,10 +209,16 @@ class AppState: ObservableObject {
let wasRegularConnection = oldValue?.ipAddress != nil && oldValue?.ipAddress != "BLE"

if isRegularConnection && !wasRegularConnection {
// Regular connection established — stop BLE scanning to save power/bandwidth
if isBLEEnabled && BLECentralManager.shared.connectionStatus == .scanning {
print("[state] Regular connection active — pausing BLE scan")
BLECentralManager.shared.stopScanning()
// Regular connection established — stop BLE scanning to save power/bandwidth and disconnect active BLE
if isBLEEnabled {
if BLECentralManager.shared.connectionStatus == .scanning {
print("[state] Regular connection active — pausing BLE scan")
BLECentralManager.shared.stopScanning()
}
if BLECentralManager.shared.isAuthenticated {
print("[state] Regular connection established over Wi-Fi — disconnecting BLE active connection to shift to local network")
BLECentralManager.shared.disconnect(isManual: false)
}
}
} else if !isRegularConnection && wasRegularConnection {
// Regular connection lost — resume BLE scanning if BLE is enabled and not already BLE-connected
Expand All @@ -218,6 +228,19 @@ class AppState: ObservableObject {
BLECentralManager.shared.startScanning()
}
}

// UDP scan management: stop when regular connection is active, start when connection is lost or only BLE is active
let isRegularConnectionActive = device != nil && device?.ipAddress != "BLE"
if isSystemSleeping {
print("[state] System is sleeping — keeping UDP discovery stopped")
UDPDiscoveryManager.shared.stop()
} else if isRegularConnectionActive {
print("[state] Regular connection established — stopping UDP discovery")
UDPDiscoveryManager.shared.stop()
} else {
print("[state] No regular connection active — starting/keeping UDP discovery active")
UDPDiscoveryManager.shared.start()
}
}
}
@Published var notifications: [Notification] = []
Expand Down Expand Up @@ -504,7 +527,7 @@ class AppState: ObservableObject {
BLECentralManager.shared.startScanning()
} else {
BLECentralManager.shared.stopScanning()
BLECentralManager.shared.disconnect()
BLECentralManager.shared.disconnect(isManual: true)
}
}
}
Expand Down Expand Up @@ -957,10 +980,14 @@ class AppState: ObservableObject {
}
}

func disconnectDevice() {
func disconnectDevice(isManual: Bool = true) {
DispatchQueue.main.async {
self.isManuallyDisconnected = isManual

// Send request to remote device to disconnect
WebSocketServer.shared.sendDisconnectRequest()
if isManual {
WebSocketServer.shared.sendDisconnectRequest()
}

// Then locally reset state
self.device = nil
Expand All @@ -970,7 +997,7 @@ class AppState: ObservableObject {
self.currentDeviceWallpaperBase64 = nil

// Disconnect BLE
BLECentralManager.shared.disconnect()
BLECentralManager.shared.disconnect(isManual: isManual)

// Clean up Quick Share state
if QuickShareManager.shared.transferState != .idle {
Expand All @@ -989,6 +1016,33 @@ class AppState: ObservableObject {
}
}

func handleAutomaticDisconnect() {
DispatchQueue.main.async {
self.isManuallyDisconnected = false
self.device = nil
self.activeMacIp = nil
self.notifications.removeAll()
self.status = nil
self.currentDeviceWallpaperBase64 = nil

if QuickShareManager.shared.transferState != .idle {
QuickShareManager.shared.transferState = .idle
}

if self.adbConnected {
ADBConnector.disconnectADB()
self.adbConnected = false
}

self.showFileBrowser = false
self.browseItems.removeAll()

if BLECentralManager.shared.isAuthenticated {
self.updateVirtualDeviceForBLE()
}
}
}

// MARK: - Remote File Browser

func openFileBrowser() {
Expand Down Expand Up @@ -1505,15 +1559,6 @@ class AppState: ObservableObject {
self.status = nil
self.notifications = []
}
// Resume scanning after BLE disconnect (unless a regular connection is already active)
let hasRegularConnection = self.device?.ipAddress != nil && self.device?.ipAddress != "BLE"
if isBLEEnabled && !hasRegularConnection && !BLECentralManager.shared.isManuallyDisconnected {
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
if self.isBLEEnabled && self.device?.ipAddress != "BLE" && !BLECentralManager.shared.isAuthenticated {
BLECentralManager.shared.startScanning()
}
}
}
}
}

Expand Down
Loading