diff --git a/app/build.gradle b/app/build.gradle index 21128bba..0db13db4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,7 +38,7 @@ android { compileSdk 34 targetSdkVersion 34 versionCode 69 - versionName "5.3.13" + versionName "5.3.14" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true } diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 67f8afd5..79772ba8 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 69, - "versionName": "5.3.12", + "versionName": "5.3.14", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt index 3f39eeb8..d1082511 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt @@ -343,7 +343,7 @@ class DownloadService : Service() { return@launch } - notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30)) + notification[galleryID]?.setContentTitle(galleryInfo.title.ellipsize(32)) notify(galleryID) val queued = mutableSetOf() @@ -408,7 +408,7 @@ class DownloadService : Service() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { startForeground(R.id.downloader_notification_id, serviceNotification.build()) } else { - startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) } when (intent?.getStringExtra(KEY_COMMAND)) { @@ -433,7 +433,7 @@ class DownloadService : Service() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { startForeground(R.id.downloader_notification_id, serviceNotification.build()) } else { - startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + startForeground(R.id.downloader_notification_id, serviceNotification.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) } interceptors[Tag::class] = interceptor } diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt index a6346674..243ab320 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt @@ -12,18 +12,21 @@ import io.ktor.network.selector.SelectorManager import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel -import io.ktor.utils.io.writeStringUtf8 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import xyz.quaver.pupil.R +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class TransferClientService : Service() { private val selectorManager = SelectorManager(Dispatchers.IO) - private val channel = Channel() + private val channel = Channel>>() private var job: Job? = null private fun startForeground() = runCatching { @@ -64,13 +67,28 @@ class TransferClientService : Service() { val writeChannel = socket.openWriteChannel(autoFlush = true) runCatching { + TransferPacket.Hello().writeToChannel(writeChannel) + val handshake = TransferPacket.readFromChannel(readChannel) + + if (handshake !is TransferPacket.Hello || handshake.version != TRANSFER_PROTOCOL_VERSION) { + throw IllegalStateException("Invalid handshake") + } + while (true) { - val message = channel.receive() - Log.d("PUPILD", "Sending message $message!") - writeChannel.writeStringUtf8(message) - Log.d("PUPILD", readChannel.readUTF8Line(4).toString()) + val (packet, continuation) = channel.receive() + + Log.d("PUPILD", "Sending packet $packet") + + packet.writeToChannel(writeChannel) + + val response = TransferPacket.readFromChannel(readChannel).also { + Log.d("PUPILD", "Received packet $it") + } + + continuation.resume(response) } }.onFailure { + Log.d("PUPILD", "Connection closed with error $it") socket.close() stopSelf(startId) } @@ -85,9 +103,14 @@ class TransferClientService : Service() { } inner class Binder: android.os.Binder() { - fun sendMessage(message: String) { - Log.d("PUPILD", "Sending message $message") - channel.trySendBlocking(message + '\n') + + + suspend fun sendPacket(packet: TransferPacket): Result = runCatching { + val response = withTimeout(1000) { suspendCoroutine { continuation -> + channel.trySendBlocking(packet to continuation) + } } + + response as? TransferPacket.ListResponse ?: throw IllegalStateException("Invalid response") } } diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferProtocol.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferProtocol.kt new file mode 100644 index 00000000..264e1886 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/services/TransferProtocol.kt @@ -0,0 +1,94 @@ +package xyz.quaver.pupil.services + +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.ByteWriteChannel + +const val TRANSFER_PROTOCOL_VERSION: UByte = 1u + +enum class TransferType(val value: UByte) { + INVALID(255u), + HELLO(0u), + PING(1u), + PONG(2u), + LIST_REQUEST(3u), + LIST_RESPONSE(4u), +} + +sealed interface TransferPacket { + val type: TransferType + + suspend fun writeToChannel(channel: ByteWriteChannel) + + data class Hello(val version: UByte = TRANSFER_PROTOCOL_VERSION): TransferPacket { + override val type = TransferType.HELLO + + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + channel.writeByte(version.toByte()) + } + } + + data object Ping: TransferPacket { + override val type = TransferType.PING + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + } + } + + data object Pong: TransferPacket { + override val type = TransferType.PONG + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + } + } + + data object ListRequest: TransferPacket { + override val type = TransferType.LIST_REQUEST + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + } + } + + data object Invalid: TransferPacket { + override val type = TransferType.INVALID + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + } + } + + data class ListResponse( + val favoritesCount: Int, + val historyCount: Int, + val downloadsCount: Int, + ): TransferPacket { + override val type = TransferType.LIST_RESPONSE + + override suspend fun writeToChannel(channel: ByteWriteChannel) { + channel.writeByte(type.value.toByte()) + channel.writeInt(favoritesCount) + channel.writeInt(historyCount) + channel.writeInt(downloadsCount) + } + } + + companion object { + suspend fun readFromChannel(channel: ByteReadChannel): TransferPacket { + return when(val type = channel.readByte().toUByte()) { + TransferType.HELLO.value -> { + val version = channel.readByte().toUByte() + Hello(version) + } + TransferType.PING.value -> Ping + TransferType.PONG.value -> Pong + TransferType.LIST_REQUEST.value -> ListRequest + TransferType.LIST_RESPONSE.value -> { + val favoritesCount = channel.readInt() + val historyCount = channel.readInt() + val downloadsCount = channel.readInt() + ListResponse(favoritesCount, historyCount, downloadsCount) + } + else -> throw IllegalArgumentException("Unknown packet type: $type") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt index 1973269b..a1187159 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt @@ -14,12 +14,15 @@ import io.ktor.network.sockets.Socket import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openWriteChannel -import io.ktor.utils.io.writeStringUtf8 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import xyz.quaver.pupil.R +import xyz.quaver.pupil.favorites +import xyz.quaver.pupil.histories +import xyz.quaver.pupil.util.downloader.DownloadManager class TransferServerService : Service() { private val selectorManager = SelectorManager(Dispatchers.IO) @@ -43,15 +46,33 @@ class TransferServerService : Service() { ) } + private fun generateListResponse(): TransferPacket.ListResponse { + val favoritesCount = favorites.size + val historyCount = histories.size + val downloadsCount = DownloadManager.getInstance(this).downloadFolderMap.size + return TransferPacket.ListResponse(favoritesCount, historyCount, downloadsCount) + } + private suspend fun handleConnection(socket: Socket) { val readChannel = socket.openReadChannel() val writeChannel = socket.openWriteChannel(autoFlush = true) runCatching { while (true) { - if (readChannel.readUTF8Line(8) == "ping") { - writeChannel.writeStringUtf8("pong\n") + val packet = TransferPacket.readFromChannel(readChannel) + + Log.d("PUPILD", "Received packet $packet") + + binder.channel.trySend(packet) + + val response = when (packet) { + is TransferPacket.Hello -> TransferPacket.Hello() + is TransferPacket.Ping -> TransferPacket.Pong + is TransferPacket.ListRequest -> generateListResponse() + else -> TransferPacket.Invalid } + + response.writeToChannel(writeChannel) } }.onFailure { socket.close() @@ -93,7 +114,7 @@ class TransferServerService : Service() { } inner class Binder: android.os.Binder() { - fun getService() = this@TransferServerService + val channel = Channel() } private val binder = Binder() diff --git a/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt index 95b3383c..ca04e026 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -44,6 +43,7 @@ import kotlinx.coroutines.launch import xyz.quaver.pupil.R import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver import xyz.quaver.pupil.services.TransferClientService +import xyz.quaver.pupil.services.TransferPacket import xyz.quaver.pupil.services.TransferServerService import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment @@ -81,15 +81,15 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } } - private var serviceBinder: TransferClientService.Binder? = null + private var clientServiceBinder: TransferClientService.Binder? = null - private val serviceConnection = object : ServiceConnection { + private val clientServiceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - serviceBinder = service as TransferClientService.Binder + clientServiceBinder = service as TransferClientService.Binder } override fun onServiceDisconnected(name: ComponentName?) { - serviceBinder = null + clientServiceBinder = null } } @@ -118,6 +118,17 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { return true } + private fun handleServerResponse(response: TransferPacket?) { + when (response) { + is TransferPacket.ListResponse -> { + Log.d("PUPILD", "Received list response $response") + } + else -> { + Log.d("PUPILD", "Received invalid response $response") + } + } + } + @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -146,11 +157,12 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } lifecycleScope.launch { viewModel.messageQueue.consumeEach { - serviceBinder?.sendMessage(it) + clientServiceBinder?.sendPacket(it)?.getOrNull()?.let(::handleServerResponse) } + } - viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).cancellable().collect step@{ step -> - Log.d("PUPILD", "Step: $step") + lifecycleScope.launch { + viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest step@{ step -> when (step) { TransferStep.TARGET, TransferStep.TARGET_FORCE -> { @@ -176,7 +188,8 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { val intent = Intent(this@TransferActivity, TransferClientService::class.java).also { it.putExtra("address", hostAddress) } - bindService(intent, serviceConnection, BIND_AUTO_CREATE) + ContextCompat.startForegroundService(this@TransferActivity, intent) + bindService(intent, clientServiceConnection, BIND_AUTO_CREATE) viewModel.setStep(TransferStep.SELECT_DATA) } @@ -243,6 +256,16 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { it.putExtra("address", address) } ContextCompat.startForegroundService(this@TransferActivity, intent) + val binder: TransferServerService.Binder = suspendCoroutine { continuation -> + bindService(intent, object: ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + continuation.resume(service as TransferServerService.Binder) + } + + override fun onServiceDisconnected(name: ComponentName?) { } + }, BIND_AUTO_CREATE) + } + binder.channel.receive() viewModel.setStep(TransferStep.CONNECTED) }.onFailure { @@ -271,6 +294,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { override fun onResume() { super.onResume() + bindService(Intent(this, TransferClientService::class.java), clientServiceConnection, BIND_AUTO_CREATE) WifiDirectBroadcastReceiver(manager, channel, viewModel).also { receiver = it registerReceiver(it, intentFilter) @@ -279,6 +303,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { override fun onPause() { super.onPause() + unbindService(clientServiceConnection) receiver?.let { unregisterReceiver(it) } receiver = null } @@ -313,7 +338,7 @@ class TransferViewModel : ViewModel() { private val _peerToConnect: MutableLiveData = MutableLiveData(null) val peerToConnect: LiveData = _peerToConnect - val messageQueue: Channel = Channel() + val messageQueue: Channel = Channel() fun setStep(step: TransferStep) { Log.d("PUPILD", "Set step: $step") @@ -345,6 +370,10 @@ class TransferViewModel : ViewModel() { } fun ping() { - messageQueue.trySend("ping") + messageQueue.trySend(TransferPacket.Ping) + } + + fun list() { + messageQueue.trySend(TransferPacket.ListRequest) } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt index c71f3f4b..727089bb 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt @@ -24,7 +24,7 @@ class TransferSelectDataFragment: Fragment() { _binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false) binding.checkAll.setOnCheckedChangeListener { _, isChecked -> - viewModel.ping() + viewModel.list() } return binding.root diff --git a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt index c868c24e..a696b4f2 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/downloader/DownloadManager.kt @@ -20,6 +20,7 @@ package xyz.quaver.pupil.util.downloader import android.content.Context import android.content.ContextWrapper +import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt index 117fed4a..327ba3f8 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt @@ -24,6 +24,7 @@ import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.os.Build +import android.util.Log import androidx.activity.result.ActivityResultLauncher import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat @@ -136,18 +137,20 @@ fun byteCount(codePoint: Int): Int = when (codePoint) { fun String.ellipsize(n: Int): String = buildString { var count = 0 var index = 0 - val codePointLength = this.codePointCount(0, this.length) + val codePointLength = this@ellipsize.codePointCount(0, this@ellipsize.length) while (index < codePointLength) { - val next = this.codePointAt(index) - if (count + next > 124) { + val nextCodePoint = this@ellipsize.codePointAt(index) + val nextByte = byteCount(nextCodePoint) + if (count + nextByte > 124) { append("…") break } - appendCodePoint(next) - count += next + appendCodePoint(nextCodePoint) + count += nextByte index++ } + } operator fun JsonElement.get(index: Int) = diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 9446e355..1be8aabf 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -52,9 +52,9 @@ app:defaultValue="8" app:useSimpleSummaryProvider="true"/> - + + +