Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/src/main/java/com/sameerasw/airsync/AirSyncApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class AirSyncApp : Application() {

companion object {
private var instance: AirSyncApp? = null
fun getContext(): Application? = instance
fun isAppForeground(): Boolean = instance?.isForeground() ?: false
fun getBleConnectionManager(): com.sameerasw.airsync.data.ble.BleConnectionManager? =
instance?.bleConnectionManager
Expand Down
29 changes: 13 additions & 16 deletions app/src/main/java/com/sameerasw/airsync/data/ble/BleGattServer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,24 @@ class BleGattServer(private val context: Context) {
fun isAnyAuthenticated(): Boolean = instance?.isAuthenticated ?: false
}

init {
instance = this
}

private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter = bluetoothManager.adapter
private var gattServer: BluetoothGattServer? = null
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val dataStoreManager = DataStoreManager(context)

private var isBleSyncEnabled = true

init {
instance = this
scope.launch {
dataStoreManager.getBleSyncEnabled().collect { enabled ->
isBleSyncEnabled = enabled
}
}
}

private val _connectionState = MutableStateFlow(BleConnectionState.DISCONNECTED)
val connectionState = _connectionState.asStateFlow()

Expand Down Expand Up @@ -91,12 +98,7 @@ class BleGattServer(private val context: Context) {
return
}

val isEnabled = try {
runBlocking { dataStoreManager.getBleSyncEnabled().first() }
} catch (e: Exception) {
false
}
if (!isEnabled) {
if (!isBleSyncEnabled) {
Log.d(TAG, "BLE Sync is disabled in settings, skipping start")
return
}
Expand Down Expand Up @@ -292,12 +294,7 @@ class BleGattServer(private val context: Context) {
if (gattServer != null) BleConnectionState.ADVERTISING else BleConnectionState.DISCONNECTED
isAuthenticated = false
if (gattServer != null) {
val isEnabled = try {
runBlocking { dataStoreManager.getBleSyncEnabled().first() }
} catch (e: Exception) {
false
}
if (isEnabled) {
if (isBleSyncEnabled) {
if (!isAdvertisingPaused) {
startAdvertising()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,14 @@ object BleTransportBridge {
gattServer?.sendChunkedNotification(BleConstants.CHAR_NOTIFICATION_DISMISS_NOTIFY, id)
}

fun sendDeviceName() {
val name = android.os.Build.MODEL
gattServer?.sendChunkedNotification(BleConstants.CHAR_DEVICE_NAME, name)
fun sendDeviceName(context: android.content.Context? = null) {
val ctx = context ?: com.sameerasw.airsync.AirSyncApp.getContext() ?: return
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {
val dataStoreManager = com.sameerasw.airsync.data.local.DataStoreManager.getInstance(ctx)
val persistedName = dataStoreManager.getDeviceName().first().ifBlank { null }
val name = persistedName ?: com.sameerasw.airsync.utils.DeviceInfoUtil.getDeviceName(ctx)
gattServer?.sendChunkedNotification(BleConstants.CHAR_DEVICE_NAME, name)
}
}

// --- Inbound (Mac -> Android) ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -853,9 +853,11 @@ class AirSyncViewModel(
val target = hasNetworkAwareMappingForLastDevice()

if (currentIp == "No Wi-Fi" || currentIp == "Unknown") {
// No usable Wi‑Fi: ensure we stop any active connection and do not attempt reconnect
// No usable Wi‑Fi: ensure we stop any active Wi-Fi WebSocket connection without affecting BLE
try {
WebSocketUtil.disconnect(context)
if (WebSocketUtil.isWifiConnected()) {
WebSocketUtil.disconnect(context)
}
} catch (_: Exception) {
}
// Stop service if needed
Expand All @@ -875,8 +877,8 @@ class AirSyncViewModel(
updatePort(target.port)
updateSymmetricKey(target.symmetricKey)

// If connected/connecting to old network, disconnect first to force a clean switch
if (WebSocketUtil.isConnected() || WebSocketUtil.isConnecting()) {
// If Wi-Fi WebSocket is connected to old network, disconnect it first
if (WebSocketUtil.isWifiConnected()) {
try {
WebSocketUtil.disconnect(context)
} catch (_: Exception) {
Expand Down
65 changes: 37 additions & 28 deletions app/src/main/java/com/sameerasw/airsync/service/AirSyncService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class AirSyncService : Service() {
}
}

private var cachedLastDevice: com.sameerasw.airsync.domain.model.ConnectedDevice? = null

override fun onCreate() {
super.onCreate()
serviceInstance = this
Expand All @@ -66,6 +68,14 @@ class AirSyncService : Service() {
registerNetworkCallback()
WebSocketUtil.registerConnectionStatusListener(connectionStatusListener)

val dataStoreManager = DataStoreManager.getInstance(applicationContext)
scope.launch {
dataStoreManager.getLastConnectedDevice().collect { device ->
cachedLastDevice = device
updateNotification()
}
}

// Monitor connection status, auto-reconnect, and battery status to update notification live
scope.launch {
MacDeviceStatusManager.macDeviceStatus.collect {
Expand Down Expand Up @@ -115,23 +125,22 @@ class AirSyncService : Service() {
startForeground(NOTIFICATION_ID, buildNotification())
}

val dataStoreManager =
DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = runBlocking {
dataStoreManager.getDeviceDiscoveryEnabled().first()
}
scope.launch {
val dataStoreManager = DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = dataStoreManager.getDeviceDiscoveryEnabled().first()

// Default to PASSIVE mode to save battery
// But do a burst to check for devices immediately
DiscoveryOrchestrator.start(this, isDiscoveryEnabled)
DiscoveryOrchestrator.setDiscoveryMode(this, DiscoveryMode.PASSIVE)
DiscoveryOrchestrator.burstBroadcast(this)
// Default to PASSIVE mode to save battery
// But do a burst to check for devices immediately
DiscoveryOrchestrator.start(this@AirSyncService, isDiscoveryEnabled)
DiscoveryOrchestrator.setDiscoveryMode(this@AirSyncService, DiscoveryMode.PASSIVE)
DiscoveryOrchestrator.burstBroadcast(this@AirSyncService)

// Start WakeupService for HTTP wakeups
WakeupService.startService(this)
// Start WakeupService for HTTP wakeups
WakeupService.startService(this@AirSyncService)

// Also trigger auto-reconnect logic to check if we already have a candidate
WebSocketUtil.requestAutoReconnect(this)
// Also trigger auto-reconnect logic to check if we already have a candidate
WebSocketUtil.requestAutoReconnect(this@AirSyncService)
}
}

private fun startWebDavServer() {
Expand Down Expand Up @@ -216,19 +225,18 @@ class AirSyncService : Service() {
startForeground(NOTIFICATION_ID, buildNotification())
}

val dataStoreManager =
DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = runBlocking {
dataStoreManager.getDeviceDiscoveryEnabled().first()
}
scope.launch {
val dataStoreManager = DataStoreManager.getInstance(applicationContext)
val isDiscoveryEnabled = dataStoreManager.getDeviceDiscoveryEnabled().first()

// Keep discovery manager running for wake-ups even when connected
// But stay in Passive mode mostly
DiscoveryOrchestrator.start(this, isDiscoveryEnabled)
DiscoveryOrchestrator.setDiscoveryMode(this, DiscoveryMode.PASSIVE)
// Keep discovery manager running for wake-ups even when connected
// But stay in Passive mode mostly
DiscoveryOrchestrator.start(this@AirSyncService, isDiscoveryEnabled)
DiscoveryOrchestrator.setDiscoveryMode(this@AirSyncService, DiscoveryMode.PASSIVE)

WakeupService.startService(this)
monitorWebDavRequirements()
WakeupService.startService(this@AirSyncService)
monitorWebDavRequirements()
}
}

private fun stopSync() {
Expand Down Expand Up @@ -260,9 +268,11 @@ class AirSyncService : Service() {
// Refresh UDP socket to bind to new network interface
DiscoveryOrchestrator.refreshSocket()
// When network becomes available, do a burst to announce ourselves
if (isScanning) {
if (isScanning && !com.sameerasw.airsync.data.ble.BleGattServer.isAnyAuthenticated()) {
DiscoveryOrchestrator.burstBroadcast(applicationContext)
WebSocketUtil.requestAutoReconnect(applicationContext)
} else if (isScanning) {
DiscoveryOrchestrator.burstBroadcast(applicationContext)
}
}
}
Expand Down Expand Up @@ -319,8 +329,7 @@ class AirSyncService : Service() {
val isAuto = WebSocketUtil.isAutoReconnecting()
val isConnecting = WebSocketUtil.isConnecting()

val dataStoreManager = DataStoreManager.getInstance(applicationContext)
val lastDevice = runBlocking { dataStoreManager.getLastConnectedDevice().first() }
val lastDevice = cachedLastDevice
val macStatus = MacDeviceStatusManager.macDeviceStatus.value

if (isConnected && lastDevice != null) {
Expand Down
27 changes: 22 additions & 5 deletions app/src/main/java/com/sameerasw/airsync/utils/WakeupHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ object WakeupHandler {

val dataStoreManager = DataStoreManager.getInstance(context)

if (WebSocketUtil.isConnected()) {
Log.d(TAG, "Already connected, ignoring wake-up request")
if (WebSocketUtil.isWifiConnected()) {
Log.d(TAG, "Already connected via Wi-Fi, ignoring wake-up request")
return
}

Expand Down Expand Up @@ -115,22 +115,39 @@ object WakeupHandler {
try {
val networkDevices = dataStoreManager.getAllNetworkDeviceConnections().first()
val ourIp = DeviceInfoUtil.getWifiIpAddress(context)
val cleanMacName = cleanDeviceName(macName)

if (ourIp != null) {
val networkDevice = networkDevices.firstOrNull { device ->
device.deviceName == macName && device.getClientIpForNetwork(ourIp) == macIp
cleanDeviceName(device.deviceName) == cleanMacName && device.getClientIpForNetwork(ourIp) == macIp
}
if (networkDevice?.symmetricKey != null) return networkDevice.symmetricKey
}

val lastConnectedDevice = dataStoreManager.getLastConnectedDevice().first()
if (lastConnectedDevice?.name == macName && lastConnectedDevice.symmetricKey != null) {
if (lastConnectedDevice?.symmetricKey != null &&
(cleanDeviceName(lastConnectedDevice.name) == cleanMacName || networkDevices.size <= 1)
) {
return lastConnectedDevice.symmetricKey
}

return networkDevices.firstOrNull { it.deviceName == macName }?.symmetricKey
val matchedDevice = networkDevices.firstOrNull { cleanDeviceName(it.deviceName) == cleanMacName }
if (matchedDevice?.symmetricKey != null) return matchedDevice.symmetricKey

if (networkDevices.size == 1) {
return networkDevices.first().symmetricKey
}

return null
} catch (e: Exception) {
return null
}
}

private fun cleanDeviceName(name: String): String {
return name
.replace("’", "'")
.trim()
.lowercase()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,7 @@ object WebSocketMessageHandler {

private fun handleDisconnectRequest(context: Context) {
try {
// Mark as intentional disconnect to prevent auto-reconnect
kotlinx.coroutines.runBlocking {
CoroutineScope(Dispatchers.IO).launch {
try {
val dataStoreManager = DataStoreManager(context)
dataStoreManager.setUserManuallyDisconnected(true)
Expand Down
Loading