From 0083a0fc66202723b50a1edc8dd9d82d49458204 Mon Sep 17 00:00:00 2001 From: grendervill Date: Thu, 20 Aug 2026 00:40:00 +0300 Subject: [PATCH] =?UTF-8?q?AeroToss:=20=D1=81=D0=BA=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C,=20=D0=B2=D1=85=D0=BE=D0=B4=D1=8F=D1=89?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0=D1=87=D0=B0?= =?UTF-8?q?,=20share=20intent,=20=D0=BB=D0=B5=D0=BD=D0=B8=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=84=D0=BE=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Скорость передачи: - TransferProgress: lastUpdateTimeMs + lastBytesTransferred для расчёта - Прогресс обновляется каждые 500мс вместо каждого чанка - Скорость = (bytesDelta * 1000) / timeDelta — реальная скорость 2. Входящая передача: - HomeScreen показывает статус входящей передачи - Отдельная карточка для исходящих и входящих передач - Кнопка отмены для исходящих передач 3. Android share intent: - ACTION_SEND / ACTION_SEND_MULTIPLE в AndroidManifest - При отправке файла из проводника появляется AeroToss - Файлы копируются в temp директорию и отправляются 4. Ленивая работа в фоне: - isActive / isTransferring флаги в TcpDiscovery - Интервал broadcast: активен=3с, передача=1с, фон=15с - Экономия батареи при простое --- app/src/androidMain/AndroidManifest.xml | 10 +++ .../kotlin/com/aerotoss/MainActivity.kt | 61 ++++++++++++++++++- .../transfer/AndroidTransferManager.kt | 23 ++++--- .../com/aerotoss/core/AeroTossManager.kt | 19 ++++++ .../com/aerotoss/discovery/TcpDiscovery.kt | 6 +- .../com/aerotoss/model/TransferProgress.kt | 14 +++-- .../aerotoss/ui/navigation/AppNavigation.kt | 7 +++ .../com/aerotoss/ui/screens/HomeScreen.kt | 50 +++++++++++---- .../transfer/DesktopTransferManager.kt | 19 ++++-- 9 files changed, 178 insertions(+), 31 deletions(-) diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml index c31a86b..3aba942 100644 --- a/app/src/androidMain/AndroidManifest.xml +++ b/app/src/androidMain/AndroidManifest.xml @@ -27,6 +27,16 @@ + + + + + + + + + + () + when (intent.action) { + Intent.ACTION_SEND -> { + intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uris.add(it) } + } + Intent.ACTION_SEND_MULTIPLE -> { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let { uris.addAll(it) } + } + } + + if (uris.isNotEmpty()) { + val files = uris.mapNotNull { uri -> + copyUriToTempFile(uri) + } + manager?.setSharedFiles(files) + } + } + + private fun copyUriToTempFile(uri: Uri): File? { + return try { + val fileName = getFileName(uri) ?: "shared_file" + val tempFile = File(cacheDir, "share_${System.currentTimeMillis()}_$fileName") + contentResolver.openInputStream(uri)?.use { input -> + tempFile.outputStream().use { output -> input.copyTo(output) } + } + tempFile + } catch (_: Exception) { + null + } + } + + private fun getFileName(uri: Uri): String? { + return try { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (cursor.moveToFirst() && nameIndex >= 0) { + cursor.getString(nameIndex) + } else null + } + } catch (_: Exception) { + uri.lastPathSegment + } + } + private fun startApp() { - if (manager != null) return + if (manager != null) { + handleShareIntent(intent) + return + } startForegroundService(Intent(this, AeroTossService::class.java)) @@ -79,6 +137,7 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { val port = withContext(Dispatchers.IO) { transferManager.startServer(0) } mgr.start(port) + handleShareIntent(intent) } setContent { App(mgr) } diff --git a/app/src/androidMain/kotlin/com/aerotoss/transfer/AndroidTransferManager.kt b/app/src/androidMain/kotlin/com/aerotoss/transfer/AndroidTransferManager.kt index 68425b3..c1f60d9 100644 --- a/app/src/androidMain/kotlin/com/aerotoss/transfer/AndroidTransferManager.kt +++ b/app/src/androidMain/kotlin/com/aerotoss/transfer/AndroidTransferManager.kt @@ -242,6 +242,8 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager val sha256 = MessageDigest.getInstance("SHA-256") var bytesSent = 0L + var lastProgressTime = System.currentTimeMillis() + var lastProgressBytes = 0L val buffer = ByteArray(65536) file.inputStream().use { fis -> while (true) { @@ -250,13 +252,20 @@ class AndroidFileTransferManager(private val context: Context) : TransferManager output.write(buffer, 0, read) sha256.update(buffer, 0, read) bytesSent += read - updateOutgoingById(requestId, TransferProgress( - id = requestId, - request = request, - bytesTransferred = bytesSent, - totalBytes = file.length(), - state = TransferState.TRANSFERRING - )) + val now = System.currentTimeMillis() + if (now - lastProgressTime >= 500) { + updateOutgoingById(requestId, TransferProgress( + id = requestId, + request = request, + bytesTransferred = bytesSent, + totalBytes = file.length(), + lastUpdateTimeMs = now, + lastBytesTransferred = lastProgressBytes, + state = TransferState.TRANSFERRING + )) + lastProgressTime = now + lastProgressBytes = bytesSent + } } } output.flush() diff --git a/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt b/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt index 23111ad..6456712 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt @@ -1,8 +1,10 @@ package com.aerotoss.core import com.aerotoss.discovery.DiscoveryManager +import com.aerotoss.discovery.TcpDiscovery import com.aerotoss.model.Device import com.aerotoss.model.TransferProgress +import com.aerotoss.model.TransferState import com.aerotoss.transfer.TransferManager import kotlinx.coroutines.flow.Flow import java.io.File @@ -18,6 +20,9 @@ class AeroTossManager( val serverPort: Int get() = transfer.getServerPort() + var sharedFiles: List = emptyList() + private set + fun start(serverPort: Int) { discovery.startDiscovery(serverPort) } @@ -28,13 +33,27 @@ class AeroTossManager( discovery.startDiscovery(port) } + fun setSharedFiles(files: List) { + sharedFiles = files + } + + fun setActive(active: Boolean) { + (discovery as? TcpDiscovery)?.isActive = active + } + + fun setTransferring(transferring: Boolean) { + (discovery as? TcpDiscovery)?.isTransferring = transferring + } + suspend fun sendFile(file: File, target: Device): Flow { require(file.exists() && file.isFile) { "File does not exist or is not a regular file" } + setTransferring(true) return transfer.sendFile(file, target.hostAddress, target.port) } fun cancelTransfer(requestId: String) { transfer.cancelTransfer(requestId) + setTransferring(false) } fun stop() { diff --git a/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt b/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt index 0f9b054..279aacf 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/discovery/TcpDiscovery.kt @@ -34,6 +34,9 @@ class TcpDiscovery( private val discoveredDevices = ConcurrentHashMap() private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true } + @Volatile var isActive = true + @Volatile var isTransferring = false + override fun startDiscovery(servicePort: Int) { if (serverThread != null) return @@ -69,7 +72,8 @@ class TcpDiscovery( var lastBroadcast = System.currentTimeMillis() while (!Thread.currentThread().isInterrupted) { val now = System.currentTimeMillis() - if (now - lastBroadcast > 1_000) { + val interval = if (isTransferring) 1_000L else if (isActive) 3_000L else 15_000L + if (now - lastBroadcast > interval) { broadcast(socket, servicePort) lastBroadcast = now } diff --git a/app/src/commonMain/kotlin/com/aerotoss/model/TransferProgress.kt b/app/src/commonMain/kotlin/com/aerotoss/model/TransferProgress.kt index 19c7ef4..f07d437 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/model/TransferProgress.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/model/TransferProgress.kt @@ -15,6 +15,8 @@ data class TransferProgress( val totalBytes: Long, val state: TransferState, val startTimeMs: Long = System.currentTimeMillis(), + val lastUpdateTimeMs: Long = System.currentTimeMillis(), + val lastBytesTransferred: Long = 0, val error: String? = null ) { val progress: Float @@ -25,11 +27,11 @@ data class TransferProgress( val speed: Long get() { - if (state != TransferState.TRANSFERRING || bytesTransferred == 0L) return 0L - val elapsed = (System.currentTimeMillis() - startTimeMs).coerceAtLeast(1) - return bytesTransferred * 1000 / elapsed + if (state != TransferState.TRANSFERRING) return 0L + val now = System.currentTimeMillis() + val timeDelta = (now - lastUpdateTimeMs).coerceAtLeast(1) + val bytesDelta = bytesTransferred - lastBytesTransferred + if (bytesDelta <= 0) return 0L + return bytesDelta * 1000 / timeDelta } - - val elapsedMs: Long - get() = System.currentTimeMillis() - startTimeMs } diff --git a/app/src/commonMain/kotlin/com/aerotoss/ui/navigation/AppNavigation.kt b/app/src/commonMain/kotlin/com/aerotoss/ui/navigation/AppNavigation.kt index 3b0156d..526c547 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/ui/navigation/AppNavigation.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/ui/navigation/AppNavigation.kt @@ -11,6 +11,13 @@ fun AppNavigation(manager: AeroTossManager) { var currentScreen by remember { mutableStateOf(Screen.HOME) } var selectedDevice by remember { mutableStateOf(null) } + // Auto-navigate to send screen if shared files exist + LaunchedEffect(manager.sharedFiles) { + if (manager.sharedFiles.isNotEmpty() && currentScreen == Screen.HOME) { + currentScreen = Screen.SEND + } + } + when (currentScreen) { Screen.HOME -> HomeScreen( manager = manager, diff --git a/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt b/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt index f599cf3..337395c 100644 --- a/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt +++ b/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt @@ -23,6 +23,7 @@ fun HomeScreen( ) { val devices by manager.devices.collectAsState(initial = emptyList()) val outgoingTransfer by manager.outgoingTransfers.collectAsState(initial = null) + val incomingTransfer by manager.incomingTransfers.collectAsState(initial = null) Scaffold( topBar = { @@ -56,20 +57,47 @@ fun HomeScreen( .padding(padding) .padding(16.dp) ) { - if (outgoingTransfer != null) { - outgoingTransfer?.let { transfer -> - TransferProgressBar(transfer) - if (transfer.state == TransferState.TRANSFERRING) { - Spacer(modifier = Modifier.height(4.dp)) - OutlinedButton( - onClick = { manager.cancelTransfer(transfer.id) }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Отменить отправку") + if (incomingTransfer != null) { + incomingTransfer?.let { transfer -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text("Входящая передача:", style = MaterialTheme.typography.titleSmall) + TransferProgressBar(transfer) } } + Spacer(modifier = Modifier.height(8.dp)) + } + } + + if (outgoingTransfer != null) { + outgoingTransfer?.let { transfer -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { + Column(modifier = Modifier.padding(12.dp)) { + Text("Исходящая передача:", style = MaterialTheme.typography.titleSmall) + TransferProgressBar(transfer) + if (transfer.state == TransferState.TRANSFERRING) { + Spacer(modifier = Modifier.height(4.dp)) + OutlinedButton( + onClick = { manager.cancelTransfer(transfer.id) }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Отменить") + } + } + } + } + Spacer(modifier = Modifier.height(8.dp)) } - Spacer(modifier = Modifier.height(8.dp)) } if (devices.isEmpty()) { diff --git a/app/src/desktopMain/kotlin/com/aerotoss/transfer/DesktopTransferManager.kt b/app/src/desktopMain/kotlin/com/aerotoss/transfer/DesktopTransferManager.kt index e6c30d7..db6bb8e 100644 --- a/app/src/desktopMain/kotlin/com/aerotoss/transfer/DesktopTransferManager.kt +++ b/app/src/desktopMain/kotlin/com/aerotoss/transfer/DesktopTransferManager.kt @@ -240,6 +240,8 @@ class DesktopFileTransferManager : TransferManager { val sha256 = MessageDigest.getInstance("SHA-256") var bytesSent = 0L + var lastProgressTime = System.currentTimeMillis() + var lastProgressBytes = 0L val buffer = ByteArray(65536) file.inputStream().use { fis -> while (true) { @@ -248,11 +250,18 @@ class DesktopFileTransferManager : TransferManager { output.write(buffer, 0, read) sha256.update(buffer, 0, read) bytesSent += read - stateFlow.value = stateFlow.value.copy( - bytesTransferred = bytesSent, - state = TransferState.TRANSFERRING - ) - updateOutgoing(stateFlow.value) + val now = System.currentTimeMillis() + if (now - lastProgressTime >= 500) { + stateFlow.value = stateFlow.value.copy( + bytesTransferred = bytesSent, + lastUpdateTimeMs = now, + lastBytesTransferred = lastProgressBytes, + state = TransferState.TRANSFERRING + ) + updateOutgoing(stateFlow.value) + lastProgressTime = now + lastProgressBytes = bytesSent + } } } output.flush()