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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -50,6 +51,7 @@ class InboundQuickShareConnection(
}

private val executor = Executors.newSingleThreadExecutor()
private val finished = AtomicBoolean(false)
private var isRunning = true
private var encryptionActive = false

Expand All @@ -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()
}
}
}
Expand Down Expand Up @@ -325,7 +327,7 @@ class InboundQuickShareConnection(
break
}
}
close()
closeConnection()
}

private fun handleSharingFrame(frame: Frame) {
Expand Down Expand Up @@ -367,11 +369,11 @@ class InboundQuickShareConnection(
)
)

writeSharingFrame(frame)

if (status == SharingResponse.Status.ACCEPT) {
openFiles()
}

writeSharingFrame(frame)
}

private fun prepareFiles(intro: IntroductionFrame) {
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class QuickShareServer(
socket = socket
)
onNewConnection(connection)
connection.start()
}
} catch (e: Exception) {
if (isRunning) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<String, InboundQuickShareConnection>()
private val activeConnections = ConcurrentHashMap<String, InboundQuickShareConnection>()
private val binder = LocalBinder()
private val serviceScope = CoroutineScope(Dispatchers.IO)
private var discoveryJob: kotlinx.coroutines.Job? = null
Expand Down Expand Up @@ -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(
Expand Down