diff --git a/app/src/androidMain/AndroidManifest.xml b/app/src/androidMain/AndroidManifest.xml
index 3ceae8d..c31a86b 100644
--- a/app/src/androidMain/AndroidManifest.xml
+++ b/app/src/androidMain/AndroidManifest.xml
@@ -10,6 +10,10 @@
+
+
+
+
+
diff --git a/app/src/androidMain/kotlin/com/aerotoss/AeroTossService.kt b/app/src/androidMain/kotlin/com/aerotoss/AeroTossService.kt
new file mode 100644
index 0000000..2833858
--- /dev/null
+++ b/app/src/androidMain/kotlin/com/aerotoss/AeroTossService.kt
@@ -0,0 +1,64 @@
+package com.aerotoss
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.os.PowerManager
+
+class AeroTossService : Service() {
+ private var wakeLock: PowerManager.WakeLock? = null
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ val notification = Notification.Builder(this, CHANNEL_ID)
+ .setContentTitle("AeroToss")
+ .setContentText("Поиск устройств...")
+ .setSmallIcon(android.R.drawable.ic_menu_share)
+ .build()
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+ } else {
+ @Suppress("DEPRECATION")
+ startForeground(NOTIFICATION_ID, notification)
+ }
+
+ @Suppress("DEPRECATION")
+ val pm = getSystemService(POWER_SERVICE) as PowerManager
+ @Suppress("DEPRECATION")
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AeroToss::DiscoveryLock")
+ wakeLock?.acquire(60 * 60 * 1000L)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ wakeLock?.release()
+ super.onDestroy()
+ }
+
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "AeroToss Discovery",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+ nm.createNotificationChannel(channel)
+ }
+
+ companion object {
+ const val CHANNEL_ID = "aerotoss_discovery"
+ const val NOTIFICATION_ID = 1
+ }
+}
diff --git a/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt b/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt
index 6b98387..f0d6790 100644
--- a/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt
+++ b/app/src/androidMain/kotlin/com/aerotoss/MainActivity.kt
@@ -1,9 +1,14 @@
package com.aerotoss
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.aerotoss.core.AeroTossManager
import com.aerotoss.discovery.CompositeAndroidDiscovery
@@ -25,14 +30,46 @@ class MainActivity : ComponentActivity() {
handleAndroidPickResult(uri, applicationContext)
}
+ private val permissionLauncher = registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { permissions ->
+ if (permissions.values.all { it }) {
+ startApp()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- if (manager != null) return
androidFilePickerLauncher = { mimeTypes, _ ->
filePickerLauncher.launch(mimeTypes)
}
+ val neededPermissions = mutableListOf(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.CHANGE_WIFI_MULTICAST_STATE,
+ )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ neededPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ }
+
+ val notGranted = neededPermissions.filter {
+ ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
+ }
+
+ if (notGranted.isNotEmpty()) {
+ permissionLauncher.launch(notGranted.toTypedArray())
+ } else {
+ startApp()
+ }
+ }
+
+ private fun startApp() {
+ if (manager != null) return
+
+ startForegroundService(Intent(this, AeroTossService::class.java))
+
val transferManager = AndroidFileTransferManager(applicationContext)
val wifiDirectManager = WifiDirectManager(applicationContext)
val discoveryManager = CompositeAndroidDiscovery(wifiDirectManager)
@@ -51,5 +88,6 @@ class MainActivity : ComponentActivity() {
super.onDestroy()
manager?.dispose()
manager = null
+ stopService(Intent(this, AeroTossService::class.java))
}
}
diff --git a/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt b/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt
index 23111ad..75487e6 100644
--- a/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt
+++ b/app/src/commonMain/kotlin/com/aerotoss/core/AeroTossManager.kt
@@ -4,6 +4,7 @@ import com.aerotoss.discovery.DiscoveryManager
import com.aerotoss.model.Device
import com.aerotoss.model.TransferProgress
import com.aerotoss.transfer.TransferManager
+import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import java.io.File
@@ -18,14 +19,34 @@ class AeroTossManager(
val serverPort: Int
get() = transfer.getServerPort()
+ private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
+ private var autoScanJob: Job? = null
+
fun start(serverPort: Int) {
discovery.startDiscovery(serverPort)
+ startAutoScan()
+ }
+
+ private fun startAutoScan() {
+ autoScanJob?.cancel()
+ autoScanJob = scope.launch {
+ while (isActive) {
+ delay(5_000)
+ val port = transfer.getServerPort()
+ if (port > 0) {
+ discovery.stopDiscovery()
+ discovery.startDiscovery(port)
+ }
+ }
+ }
}
fun rescan() {
+ autoScanJob?.cancel()
val port = transfer.getServerPort()
discovery.stopDiscovery()
discovery.startDiscovery(port)
+ startAutoScan()
}
suspend fun sendFile(file: File, target: Device): Flow {
@@ -38,10 +59,13 @@ class AeroTossManager(
}
fun stop() {
+ autoScanJob?.cancel()
discovery.stopDiscovery()
}
fun dispose() {
+ autoScanJob?.cancel()
+ scope.cancel()
discovery.dispose()
transfer.dispose()
}
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 f87f534..f599cf3 100644
--- a/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt
+++ b/app/src/commonMain/kotlin/com/aerotoss/ui/screens/HomeScreen.kt
@@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.aerotoss.core.AeroTossManager
import com.aerotoss.model.Device
+import com.aerotoss.model.TransferState
import com.aerotoss.ui.components.DeviceCard
import com.aerotoss.ui.components.TransferProgressBar
@@ -33,13 +34,9 @@ fun HomeScreen(
)
},
floatingActionButton = {
- Row(
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
FloatingActionButton(
- onClick = {
- manager.rescan()
- },
+ onClick = { manager.rescan() },
containerColor = MaterialTheme.colorScheme.tertiaryContainer
) {
Text("Обновить", style = MaterialTheme.typography.labelLarge)
@@ -62,8 +59,17 @@ fun HomeScreen(
if (outgoingTransfer != null) {
outgoingTransfer?.let { transfer ->
TransferProgressBar(transfer)
- Spacer(modifier = Modifier.height(8.dp))
+ 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))
}
if (devices.isEmpty()) {
@@ -72,19 +78,10 @@ fun HomeScreen(
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Text(
- "Поиск устройств...",
- style = MaterialTheme.typography.bodyLarge
- )
+ Text("Поиск устройств...", style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(16.dp))
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
- OutlinedButton(
- onClick = { manager.rescan() }
- ) {
- Text("Сканировать заново")
- }
- Spacer(modifier = Modifier.height(8.dp))
Text(
"Убедитесь, что другое устройство\nтакже запустило AeroToss\nв одной Wi-Fi сети",
style = MaterialTheme.typography.bodySmall,
@@ -93,27 +90,15 @@ fun HomeScreen(
}
}
} else {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- "Найдено: ${devices.size}",
- style = MaterialTheme.typography.titleSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- TextButton(onClick = { manager.rescan() }) {
- Text("Обновить")
- }
- }
+ Text(
+ "Найдено: ${devices.size}",
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(devices, key = { it.id }) { device ->
- DeviceCard(
- device = device,
- onClick = { onSendClick(device) }
- )
+ DeviceCard(device = device, onClick = { onSendClick(device) })
}
}
}
diff --git a/app/src/commonMain/kotlin/com/aerotoss/ui/screens/SendScreen.kt b/app/src/commonMain/kotlin/com/aerotoss/ui/screens/SendScreen.kt
index 1ffc119..8fc10db 100644
--- a/app/src/commonMain/kotlin/com/aerotoss/ui/screens/SendScreen.kt
+++ b/app/src/commonMain/kotlin/com/aerotoss/ui/screens/SendScreen.kt
@@ -71,6 +71,18 @@ fun SendScreen(
when {
outgoingTransfer?.state == TransferState.TRANSFERRING -> {
outgoingTransfer?.let { TransferProgressBar(it) }
+ Spacer(modifier = Modifier.height(8.dp))
+ OutlinedButton(
+ onClick = {
+ outgoingTransfer?.let { manager.cancelTransfer(it.id) }
+ selectedFile = null
+ isSending = false
+ },
+ modifier = Modifier.fillMaxWidth(),
+ colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error)
+ ) {
+ Text("Отменить отправку")
+ }
}
outgoingTransfer?.state == TransferState.COMPLETED -> {
Text("Отправлено!", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary)