From e453f87743ceb11d0afc7a2c1c8e06b35952fac1 Mon Sep 17 00:00:00 2001 From: hkay-dev <14947763+hkay-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:28:40 -0700 Subject: [PATCH 1/2] Fix inbound Quick Share file completion --- .../quickshare/InboundQuickShareConnection.kt | 37 ++++++++++++------- .../airsync/quickshare/QuickShareServer.kt | 1 + 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt index d7d2352c..fc854da4 100644 --- a/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt +++ b/app/src/main/java/com/sameerasw/airsync/quickshare/InboundQuickShareConnection.kt @@ -25,6 +25,7 @@ import java.io.FileOutputStream import java.net.Socket import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean import com.google.android.gms.nearby.sharing.ConnectionResponseFrame as SharingResponse import com.google.android.gms.nearby.sharing.V1Frame as SharingV1 @@ -50,6 +51,7 @@ class InboundQuickShareConnection( } private val executor = Executors.newSingleThreadExecutor() + private val finished = AtomicBoolean(false) private var isRunning = true private var encryptionActive = false @@ -67,17 +69,17 @@ class InboundQuickShareConnection( val size: Long, var bytesTransferred: Long = 0, var file: File? = null, - var outputStream: java.io.OutputStream? = null, + @Volatile var outputStream: java.io.OutputStream? = null, var uri: Uri? = null ) - init { + fun start() { executor.execute { try { runHandshake() } catch (e: Exception) { Log.e(TAG, "Handshake failed", e) - close() + closeConnection() } } } @@ -325,7 +327,7 @@ class InboundQuickShareConnection( break } } - close() + closeConnection() } private fun handleSharingFrame(frame: Frame) { @@ -367,11 +369,11 @@ class InboundQuickShareConnection( ) ) - writeSharingFrame(frame) - if (status == SharingResponse.Status.ACCEPT) { openFiles() } + + writeSharingFrame(frame) } private fun prepareFiles(intro: IntroductionFrame) { @@ -396,8 +398,8 @@ class InboundQuickShareConnection( values ) if (uri != null) { - info.outputStream = context.contentResolver.openOutputStream(uri) info.uri = uri + info.outputStream = context.contentResolver.openOutputStream(uri) Log.d(TAG, "Prepared file via MediaStore: ${info.name} -> $uri") } } else { @@ -424,10 +426,11 @@ class InboundQuickShareConnection( val id = payloadTransfer.payload_header?.id ?: return val chunk = payloadTransfer.payload_chunk ?: return val info = transferredFiles[id] ?: return + val outputStream = checkNotNull(info.outputStream) { "No output stream for ${info.name}" } val body = chunk.body?.toByteArray() if (body != null && body.isNotEmpty()) { - info.outputStream?.write(body) + outputStream.write(body) info.bytesTransferred += body.size // Update progress (throttle if needed, but for now simple) @@ -445,8 +448,9 @@ class InboundQuickShareConnection( // Check last chunk flag (flags & 1) if ((chunk.flags ?: 0) and 1 != 0) { + check(info.bytesTransferred == info.size) { "Incomplete file: ${info.name}" } Log.d(TAG, "File ${info.name} transfer complete (${info.bytesTransferred} bytes)") - info.outputStream?.close() + outputStream.close() info.outputStream = null // Clear IS_PENDING so file becomes visible in Downloads @@ -462,22 +466,29 @@ class InboundQuickShareConnection( // Check if all files are finished if (transferredFiles.values.all { it.outputStream == null }) { Log.d(TAG, "All files transferred") - onFinished?.invoke(this) + finish() } } } fun closeConnection() { - val wasRunning = isRunning isRunning = false - super.close() + try { + super.close() + } catch (_: Exception) { + // May already be closed after a failed handshake. + } try { socket.close() } catch (e: Exception) { // Ignore } executor.shutdownNow() - if (wasRunning) { + finish() + } + + private fun finish() { + if (finished.compareAndSet(false, true)) { onFinished?.invoke(this) } } diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt index ffeca54b..376b0352 100644 --- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt +++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareServer.kt @@ -43,6 +43,7 @@ class QuickShareServer( socket = socket ) onNewConnection(connection) + connection.start() } } catch (e: Exception) { if (isRunning) { From 516c2f16655789e8472c2b89ce7ce66145121589 Mon Sep 17 00:00:00 2001 From: hkay-dev <14947763+hkay-dev@users.noreply.github.com> Date: Wed, 23 Sep 2026 02:28:41 -0700 Subject: [PATCH 2/2] Avoid duplicate Quick Share discovery --- .../sameerasw/airsync/quickshare/QuickShareService.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt index 2629c86e..79f417b7 100644 --- a/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt +++ b/app/src/main/java/com/sameerasw/airsync/quickshare/QuickShareService.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap /** * Foreground service that manages Quick Share advertisement and connections. @@ -41,7 +42,7 @@ class QuickShareService : Service() { private lateinit var server: QuickShareServer private lateinit var advertiser: QuickShareAdvertiser private lateinit var dataStoreManager: DataStoreManager - private val activeConnections = mutableMapOf() + private val activeConnections = ConcurrentHashMap() private val binder = LocalBinder() private val serviceScope = CoroutineScope(Dispatchers.IO) private var discoveryJob: kotlinx.coroutines.Job? = null @@ -251,8 +252,12 @@ class QuickShareService : Service() { return START_STICKY } + @Synchronized private fun startDiscoveryWithTimeout() { - discoveryJob?.cancel() + if (discoveryJob?.isActive == true) { + Log.d(TAG, "Discovery already active") + return + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground(