fixed downloading galleris with long title, [wip] transfer data

This commit is contained in:
tom5079
2024-04-17 23:25:36 -07:00
parent b0e194898e
commit 68ec919ae4
11 changed files with 209 additions and 38 deletions

View File

@@ -38,7 +38,7 @@ android {
compileSdk 34 compileSdk 34
targetSdkVersion 34 targetSdkVersion 34
versionCode 69 versionCode 69
versionName "5.3.13" versionName "5.3.14"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

@@ -12,7 +12,7 @@
"filters": [], "filters": [],
"attributes": [], "attributes": [],
"versionCode": 69, "versionCode": 69,
"versionName": "5.3.12", "versionName": "5.3.14",
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],

View File

@@ -343,7 +343,7 @@ class DownloadService : Service() {
return@launch return@launch
} }
notification[galleryID]?.setContentTitle(galleryInfo.title?.ellipsize(30)) notification[galleryID]?.setContentTitle(galleryInfo.title.ellipsize(32))
notify(galleryID) notify(galleryID)
val queued = mutableSetOf<Int>() val queued = mutableSetOf<Int>()
@@ -408,7 +408,7 @@ class DownloadService : Service() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
startForeground(R.id.downloader_notification_id, serviceNotification.build()) startForeground(R.id.downloader_notification_id, serviceNotification.build())
} else { } 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)) { when (intent?.getStringExtra(KEY_COMMAND)) {
@@ -433,7 +433,7 @@ class DownloadService : Service() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
startForeground(R.id.downloader_notification_id, serviceNotification.build()) startForeground(R.id.downloader_notification_id, serviceNotification.build())
} else { } 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 interceptors[Tag::class] = interceptor
} }

View File

@@ -12,18 +12,21 @@ import io.ktor.network.selector.SelectorManager
import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.writeStringUtf8
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TransferClientService : Service() { class TransferClientService : Service() {
private val selectorManager = SelectorManager(Dispatchers.IO) private val selectorManager = SelectorManager(Dispatchers.IO)
private val channel = Channel<String>() private val channel = Channel<Pair<TransferPacket, Continuation<TransferPacket>>>()
private var job: Job? = null private var job: Job? = null
private fun startForeground() = runCatching { private fun startForeground() = runCatching {
@@ -64,13 +67,28 @@ class TransferClientService : Service() {
val writeChannel = socket.openWriteChannel(autoFlush = true) val writeChannel = socket.openWriteChannel(autoFlush = true)
runCatching { 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) { while (true) {
val message = channel.receive() val (packet, continuation) = channel.receive()
Log.d("PUPILD", "Sending message $message!")
writeChannel.writeStringUtf8(message) Log.d("PUPILD", "Sending packet $packet")
Log.d("PUPILD", readChannel.readUTF8Line(4).toString())
packet.writeToChannel(writeChannel)
val response = TransferPacket.readFromChannel(readChannel).also {
Log.d("PUPILD", "Received packet $it")
}
continuation.resume(response)
} }
}.onFailure { }.onFailure {
Log.d("PUPILD", "Connection closed with error $it")
socket.close() socket.close()
stopSelf(startId) stopSelf(startId)
} }
@@ -85,9 +103,14 @@ class TransferClientService : Service() {
} }
inner class Binder: android.os.Binder() { 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<TransferPacket.ListResponse> = runCatching {
val response = withTimeout(1000) { suspendCoroutine { continuation ->
channel.trySendBlocking(packet to continuation)
} }
response as? TransferPacket.ListResponse ?: throw IllegalStateException("Invalid response")
} }
} }

View File

@@ -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")
}
}
}
}

View File

@@ -14,12 +14,15 @@ import io.ktor.network.sockets.Socket
import io.ktor.network.sockets.aSocket import io.ktor.network.sockets.aSocket
import io.ktor.network.sockets.openReadChannel import io.ktor.network.sockets.openReadChannel
import io.ktor.network.sockets.openWriteChannel import io.ktor.network.sockets.openWriteChannel
import io.ktor.utils.io.writeStringUtf8
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import xyz.quaver.pupil.R 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() { class TransferServerService : Service() {
private val selectorManager = SelectorManager(Dispatchers.IO) 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) { private suspend fun handleConnection(socket: Socket) {
val readChannel = socket.openReadChannel() val readChannel = socket.openReadChannel()
val writeChannel = socket.openWriteChannel(autoFlush = true) val writeChannel = socket.openWriteChannel(autoFlush = true)
runCatching { runCatching {
while (true) { while (true) {
if (readChannel.readUTF8Line(8) == "ping") { val packet = TransferPacket.readFromChannel(readChannel)
writeChannel.writeStringUtf8("pong\n")
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 { }.onFailure {
socket.close() socket.close()
@@ -93,7 +114,7 @@ class TransferServerService : Service() {
} }
inner class Binder: android.os.Binder() { inner class Binder: android.os.Binder() {
fun getService() = this@TransferServerService val channel = Channel<TransferPacket>()
} }
private val binder = Binder() private val binder = Binder()

View File

@@ -36,7 +36,6 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -44,6 +43,7 @@ import kotlinx.coroutines.launch
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver
import xyz.quaver.pupil.services.TransferClientService import xyz.quaver.pupil.services.TransferClientService
import xyz.quaver.pupil.services.TransferPacket
import xyz.quaver.pupil.services.TransferServerService import xyz.quaver.pupil.services.TransferServerService
import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment
import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment 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?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
serviceBinder = service as TransferClientService.Binder clientServiceBinder = service as TransferClientService.Binder
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
serviceBinder = null clientServiceBinder = null
} }
} }
@@ -118,6 +118,17 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
return true 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") @SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -146,11 +157,12 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
} }
lifecycleScope.launch { lifecycleScope.launch {
viewModel.messageQueue.consumeEach { viewModel.messageQueue.consumeEach {
serviceBinder?.sendMessage(it) clientServiceBinder?.sendPacket(it)?.getOrNull()?.let(::handleServerResponse)
}
} }
viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).cancellable().collect step@{ step -> lifecycleScope.launch {
Log.d("PUPILD", "Step: $step") viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest step@{ step ->
when (step) { when (step) {
TransferStep.TARGET, TransferStep.TARGET,
TransferStep.TARGET_FORCE -> { TransferStep.TARGET_FORCE -> {
@@ -176,7 +188,8 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
val intent = Intent(this@TransferActivity, TransferClientService::class.java).also { val intent = Intent(this@TransferActivity, TransferClientService::class.java).also {
it.putExtra("address", hostAddress) 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) viewModel.setStep(TransferStep.SELECT_DATA)
} }
@@ -243,6 +256,16 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
it.putExtra("address", address) it.putExtra("address", address)
} }
ContextCompat.startForegroundService(this@TransferActivity, intent) 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) viewModel.setStep(TransferStep.CONNECTED)
}.onFailure { }.onFailure {
@@ -271,6 +294,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
bindService(Intent(this, TransferClientService::class.java), clientServiceConnection, BIND_AUTO_CREATE)
WifiDirectBroadcastReceiver(manager, channel, viewModel).also { WifiDirectBroadcastReceiver(manager, channel, viewModel).also {
receiver = it receiver = it
registerReceiver(it, intentFilter) registerReceiver(it, intentFilter)
@@ -279,6 +303,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
unbindService(clientServiceConnection)
receiver?.let { unregisterReceiver(it) } receiver?.let { unregisterReceiver(it) }
receiver = null receiver = null
} }
@@ -313,7 +338,7 @@ class TransferViewModel : ViewModel() {
private val _peerToConnect: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null) private val _peerToConnect: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
val peerToConnect: LiveData<WifiP2pDevice?> = _peerToConnect val peerToConnect: LiveData<WifiP2pDevice?> = _peerToConnect
val messageQueue: Channel<String> = Channel() val messageQueue: Channel<TransferPacket> = Channel()
fun setStep(step: TransferStep) { fun setStep(step: TransferStep) {
Log.d("PUPILD", "Set step: $step") Log.d("PUPILD", "Set step: $step")
@@ -345,6 +370,10 @@ class TransferViewModel : ViewModel() {
} }
fun ping() { fun ping() {
messageQueue.trySend("ping") messageQueue.trySend(TransferPacket.Ping)
}
fun list() {
messageQueue.trySend(TransferPacket.ListRequest)
} }
} }

View File

@@ -24,7 +24,7 @@ class TransferSelectDataFragment: Fragment() {
_binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false) _binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false)
binding.checkAll.setOnCheckedChangeListener { _, isChecked -> binding.checkAll.setOnCheckedChangeListener { _, isChecked ->
viewModel.ping() viewModel.list()
} }
return binding.root return binding.root

View File

@@ -20,6 +20,7 @@ package xyz.quaver.pupil.util.downloader
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -24,6 +24,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.util.Log
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@@ -136,18 +137,20 @@ fun byteCount(codePoint: Int): Int = when (codePoint) {
fun String.ellipsize(n: Int): String = buildString { fun String.ellipsize(n: Int): String = buildString {
var count = 0 var count = 0
var index = 0 var index = 0
val codePointLength = this.codePointCount(0, this.length) val codePointLength = this@ellipsize.codePointCount(0, this@ellipsize.length)
while (index < codePointLength) { while (index < codePointLength) {
val next = this.codePointAt(index) val nextCodePoint = this@ellipsize.codePointAt(index)
if (count + next > 124) { val nextByte = byteCount(nextCodePoint)
if (count + nextByte > 124) {
append("") append("")
break break
} }
appendCodePoint(next) appendCodePoint(nextCodePoint)
count += next count += nextByte
index++ index++
} }
} }
operator fun JsonElement.get(index: Int) = operator fun JsonElement.get(index: Int) =

View File

@@ -52,9 +52,9 @@
app:defaultValue="8" app:defaultValue="8"
app:useSimpleSummaryProvider="true"/> app:useSimpleSummaryProvider="true"/>
<Preference <!-- <Preference-->
app:key="transfer_data" <!-- app:key="transfer_data"-->
app:title="@string/settings_transfer_data"/> <!-- app:title="@string/settings_transfer_data"/>-->
<SwitchPreferenceCompat <SwitchPreferenceCompat
app:key="nomedia" app:key="nomedia"