AeroToss: скорость, входящая передача, share intent, ленивый фон
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с - Экономия батареи при простое
This commit is contained in:
@@ -27,6 +27,16 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".AeroTossService"
|
||||
|
||||
@@ -3,8 +3,10 @@ package com.aerotoss
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -20,6 +22,7 @@ import com.aerotoss.wifi.WifiDirectManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var manager: AeroTossManager? = null
|
||||
@@ -65,8 +68,63 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleShareIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleShareIntent(intent: Intent?) {
|
||||
if (intent?.action != Intent.ACTION_SEND && intent?.action != Intent.ACTION_SEND_MULTIPLE) return
|
||||
|
||||
val uris = mutableListOf<Uri>()
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { uris.add(it) }
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
intent.getParcelableArrayListExtra<Uri>(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) }
|
||||
|
||||
@@ -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
|
||||
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()
|
||||
|
||||
@@ -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<File> = emptyList()
|
||||
private set
|
||||
|
||||
fun start(serverPort: Int) {
|
||||
discovery.startDiscovery(serverPort)
|
||||
}
|
||||
@@ -28,13 +33,27 @@ class AeroTossManager(
|
||||
discovery.startDiscovery(port)
|
||||
}
|
||||
|
||||
fun setSharedFiles(files: List<File>) {
|
||||
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<TransferProgress> {
|
||||
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() {
|
||||
|
||||
@@ -34,6 +34,9 @@ class TcpDiscovery(
|
||||
private val discoveredDevices = ConcurrentHashMap<String, Device>()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ fun AppNavigation(manager: AeroTossManager) {
|
||||
var currentScreen by remember { mutableStateOf(Screen.HOME) }
|
||||
var selectedDevice by remember { mutableStateOf<com.aerotoss.model.Device?>(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,
|
||||
|
||||
@@ -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,8 +57,33 @@ fun HomeScreen(
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
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))
|
||||
@@ -65,12 +91,14 @@ fun HomeScreen(
|
||||
onClick = { manager.cancelTransfer(transfer.id) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Отменить отправку")
|
||||
Text("Отменить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
Box(
|
||||
|
||||
@@ -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
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user