From b0e194898e262836752cf78440653439d17f21d0 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:19:49 -0700 Subject: [PATCH] wip --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 14 +- app/src/main/java/xyz/quaver/pupil/Pupil.kt | 7 + .../pupil/services/TransferClientService.kt | 97 +++++++++ .../pupil/services/TransferServerService.kt | 102 +++++++++ .../xyz/quaver/pupil/ui/TransferActivity.kt | 114 +++++++--- .../ui/fragment/TransferConnectedFragment.kt | 10 + .../ui/fragment/TransferSelectDataFragment.kt | 37 ++++ app/src/main/res/drawable/check.xml | 13 ++ app/src/main/res/drawable/download.xml | 13 ++ app/src/main/res/drawable/heart.xml | 13 ++ app/src/main/res/drawable/history_rounded.xml | 9 + .../layout/transfer_connected_fragment.xml | 28 +++ .../layout/transfer_select_data_fragment.xml | 196 ++++++++++++++++++ app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values-ko/strings.xml | 2 + app/src/main/res/values/strings.xml | 3 + 17 files changed, 632 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt create mode 100644 app/src/main/res/drawable/check.xml create mode 100644 app/src/main/res/drawable/download.xml create mode 100644 app/src/main/res/drawable/heart.xml create mode 100644 app/src/main/res/drawable/history_rounded.xml create mode 100644 app/src/main/res/layout/transfer_connected_fragment.xml create mode 100644 app/src/main/res/layout/transfer_select_data_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index bceb8214..21128bba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,13 +82,14 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2" - + + implementation "androidx.core:core-ktx:1.12.0" implementation "androidx.appcompat:appcompat:1.4.1" implementation "androidx.activity:activity-ktx:1.4.0" implementation "androidx.fragment:fragment-ktx:1.4.1" implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.gridlayout:gridlayout:1.0.0" implementation "androidx.biometric:biometric:1.1.0" implementation "androidx.work:work-runtime-ktx:2.7.1" @@ -116,6 +117,7 @@ dependencies { //noinspection GradleDependency implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation "io.ktor:ktor-network:2.3.10" implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 640c0d6d..0f98c2f7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,10 +8,10 @@ - - + + @@ -54,7 +54,15 @@ + android:foregroundServiceType="dataSync" /> + + + + ("dark_mode")) { diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt new file mode 100644 index 00000000..a6346674 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt @@ -0,0 +1,97 @@ +package xyz.quaver.pupil.services + +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +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 xyz.quaver.pupil.R + +class TransferClientService : Service() { + private val selectorManager = SelectorManager(Dispatchers.IO) + private val channel = Channel() + private var job: Job? = null + + private fun startForeground() = runCatching { + val notification = NotificationCompat.Builder(this, "transfer") + .setContentTitle("Pupil") + .setContentText("Transfer server is running") + .setSmallIcon(R.drawable.ic_notification) + .build() + + ServiceCompat.startForeground( + this, + 1, + notification, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else 0 + ) + } + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val address = intent?.getStringExtra("address") ?: run { + stopSelf(startId) + return START_STICKY + } + + startForeground() + + Log.d("PUPILD", "Starting service with address $address") + + job?.cancel() + job = CoroutineScope(Dispatchers.IO).launch { + Log.d("PUPILD", "Connecting to $address") + + val socket = aSocket(selectorManager).tcp().connect(address, 12221) + + Log.d("PUPILD", "Connected to $address") + + val readChannel = socket.openReadChannel() + val writeChannel = socket.openWriteChannel(autoFlush = true) + + runCatching { + while (true) { + val message = channel.receive() + Log.d("PUPILD", "Sending message $message!") + writeChannel.writeStringUtf8(message) + Log.d("PUPILD", readChannel.readUTF8Line(4).toString()) + } + }.onFailure { + socket.close() + stopSelf(startId) + } + } + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + job?.cancel() + } + + inner class Binder: android.os.Binder() { + fun sendMessage(message: String) { + Log.d("PUPILD", "Sending message $message") + channel.trySendBlocking(message + '\n') + } + } + + private val binder = Binder() + + override fun onBind(intent: Intent?): IBinder = binder +} \ 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 new file mode 100644 index 00000000..1973269b --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt @@ -0,0 +1,102 @@ +package xyz.quaver.pupil.services + +import android.app.Service +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import io.ktor.network.selector.SelectorManager +import io.ktor.network.sockets.ServerSocket +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.launch +import xyz.quaver.pupil.R + +class TransferServerService : Service() { + private val selectorManager = SelectorManager(Dispatchers.IO) + private var serverSocket: ServerSocket? = null + private val job = Job() + + private fun startForeground() = runCatching { + val notification = NotificationCompat.Builder(this, "transfer") + .setContentTitle("Pupil") + .setContentText("Transfer server is running") + .setSmallIcon(R.drawable.ic_notification) + .build() + + ServiceCompat.startForeground( + this, + 1, + notification, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } else 0 + ) + } + + 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") + } + } + }.onFailure { + socket.close() + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val address = intent?.getStringExtra("address") ?: run { + stopSelf(startId) + return START_STICKY + } + + if (serverSocket != null) { + return START_STICKY + } + + startForeground() + + val serverSocket = aSocket(selectorManager).tcp().bind(address, 12221).also { + this@TransferServerService.serverSocket = it + } + + CoroutineScope(Dispatchers.IO + job).launch { + while (true) { + Log.d("PUPILD", "Waiting for connection") + val socket = serverSocket.accept() + Log.d("PUPILD", "Accepted connection from ${socket.remoteAddress}") + launch { handleConnection(socket) } + } + } + + return START_STICKY + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + serverSocket?.close() + } + + inner class Binder: android.os.Binder() { + fun getService() = this@TransferServerService + } + + private val binder = Binder() + + override fun onBind(intent: Intent?): IBinder = binder +} \ No newline at end of file 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 50d61087..95b3383c 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt @@ -3,7 +3,10 @@ package xyz.quaver.pupil.ui import android.Manifest import android.annotation.SuppressLint import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Intent import android.content.IntentFilter +import android.content.ServiceConnection import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.net.wifi.WpsInfo @@ -15,11 +18,13 @@ import android.net.wifi.p2p.WifiP2pManager import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle +import android.os.IBinder import android.util.Log import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.commit import androidx.lifecycle.Lifecycle import androidx.lifecycle.LiveData @@ -27,13 +32,23 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +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 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.TransferServerService +import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment import xyz.quaver.pupil.ui.fragment.TransferPermissionFragment +import xyz.quaver.pupil.ui.fragment.TransferSelectDataFragment import xyz.quaver.pupil.ui.fragment.TransferTargetFragment import xyz.quaver.pupil.ui.fragment.TransferWaitForConnectionFragment import kotlin.coroutines.resume @@ -66,6 +81,18 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } } + private var serviceBinder: TransferClientService.Binder? = null + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + serviceBinder = service as TransferClientService.Binder + } + + override fun onServiceDisconnected(name: ComponentName?) { + serviceBinder = null + } + } + private fun checkPermission(force: Boolean = false): Boolean { val permissionRequired = if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) { Manifest.permission.ACCESS_FINE_LOCATION @@ -110,40 +137,25 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } manager.connect(channel, config, object: WifiP2pManager.ActionListener { - override fun onSuccess() { - Log.d("PUPILD", "Connection successful") - } + override fun onSuccess() { } override fun onFailure(reason: Int) { - Log.d("PUPILD", "Connection failed: $reason") - viewModel.setPeers(null) + viewModel.connect(null) } }) } - - viewModel.connectionInfo.observe(this) { info -> - if (info == null) { return@observe } - - if (info.groupFormed && info.isGroupOwner) { - // Do something - Log.d("PUPILD", "Group formed and is group owner") - Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}") - } else if (info.groupFormed) { - // Do something - Log.d("PUPILD", "Group formed") - Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}") - Log.d("PUPILD", "Local IP: ${info.groupOwnerAddress.hostAddress}") - Log.d("PUPILD", "Is group owner: ${info.isGroupOwner}") - } - } - lifecycleScope.launch { - viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { step -> + viewModel.messageQueue.consumeEach { + serviceBinder?.sendMessage(it) + } + + viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).cancellable().collect step@{ step -> + Log.d("PUPILD", "Step: $step") when (step) { TransferStep.TARGET, TransferStep.TARGET_FORCE -> { if (!checkPermission(step == TransferStep.TARGET_FORCE)) { - return@collect + return@step } manager.discoverPeers(channel, object: WifiP2pManager.ActionListener { @@ -156,6 +168,17 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { supportFragmentManager.commit { replace(R.id.fragment_container_view, TransferTargetFragment()) } + + val hostAddress = viewModel.connectionInfo.filterNotNull().first { + it.groupFormed + }.groupOwnerAddress.hostAddress + + val intent = Intent(this@TransferActivity, TransferClientService::class.java).also { + it.putExtra("address", hostAddress) + } + bindService(intent, serviceConnection, BIND_AUTO_CREATE) + + viewModel.setStep(TransferStep.SELECT_DATA) } TransferStep.DIRECTION -> { supportFragmentManager.commit { @@ -168,7 +191,8 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } } TransferStep.WAIT_FOR_CONNECTION -> { - if (!checkPermission()) { return@collect } + Log.d("PUPILD", "wait for connection") + if (!checkPermission()) { return@step } runCatching { suspendCoroutine { continuation -> @@ -206,6 +230,21 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } }) } + + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferWaitForConnectionFragment()) + } + + val address = viewModel.connectionInfo.filterNotNull().first { + it.groupFormed && it.isGroupOwner + }.groupOwnerAddress.hostAddress + + val intent = Intent(this@TransferActivity, TransferServerService::class.java).also { + it.putExtra("address", address) + } + ContextCompat.startForegroundService(this@TransferActivity, intent) + + viewModel.setStep(TransferStep.CONNECTED) }.onFailure { Log.e("PUPILD", "Failed to create group", it) } @@ -214,6 +253,16 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { replace(R.id.fragment_container_view, TransferWaitForConnectionFragment()) } } + TransferStep.CONNECTED -> { + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferConnectedFragment()) + } + } + TransferStep.SELECT_DATA -> { + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferSelectDataFragment()) + } + } } } } @@ -236,7 +285,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { } enum class TransferStep { - TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION + TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION, CONNECTED, SELECT_DATA } enum class ErrorType { @@ -258,13 +307,16 @@ class TransferViewModel : ViewModel() { private val _peers: MutableLiveData = MutableLiveData(null) val peers: LiveData = _peers - private val _connectionInfo: MutableLiveData = MutableLiveData(null) - val connectionInfo: LiveData = _connectionInfo + private val _connectionInfo: MutableStateFlow = MutableStateFlow(null) + val connectionInfo: StateFlow = _connectionInfo private val _peerToConnect: MutableLiveData = MutableLiveData(null) val peerToConnect: LiveData = _peerToConnect + val messageQueue: Channel = Channel() + fun setStep(step: TransferStep) { + Log.d("PUPILD", "Set step: $step") _step.value = step } @@ -288,7 +340,11 @@ class TransferViewModel : ViewModel() { _error.value = error } - fun connect(device: WifiP2pDevice) { + fun connect(device: WifiP2pDevice?) { _peerToConnect.value = device } + + fun ping() { + messageQueue.trySend("ping") + } } \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt new file mode 100644 index 00000000..a434d078 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt @@ -0,0 +1,10 @@ +package xyz.quaver.pupil.ui.fragment + +import androidx.fragment.app.Fragment +import xyz.quaver.pupil.R + +class TransferConnectedFragment: Fragment(R.layout.transfer_connected_fragment) { + + + +} \ 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 new file mode 100644 index 00000000..c71f3f4b --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt @@ -0,0 +1,37 @@ +package xyz.quaver.pupil.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import xyz.quaver.pupil.databinding.TransferSelectDataFragmentBinding +import xyz.quaver.pupil.ui.TransferViewModel + +class TransferSelectDataFragment: Fragment() { + + private var _binding: TransferSelectDataFragmentBinding? = null + private val binding get() = _binding!! + + private val viewModel: TransferViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false) + + binding.checkAll.setOnCheckedChangeListener { _, isChecked -> + viewModel.ping() + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml new file mode 100644 index 00000000..d075199a --- /dev/null +++ b/app/src/main/res/drawable/check.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml new file mode 100644 index 00000000..3f16b5b8 --- /dev/null +++ b/app/src/main/res/drawable/download.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/heart.xml b/app/src/main/res/drawable/heart.xml new file mode 100644 index 00000000..07d57bd0 --- /dev/null +++ b/app/src/main/res/drawable/heart.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/history_rounded.xml b/app/src/main/res/drawable/history_rounded.xml new file mode 100644 index 00000000..b47b45d1 --- /dev/null +++ b/app/src/main/res/drawable/history_rounded.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/transfer_connected_fragment.xml b/app/src/main/res/layout/transfer_connected_fragment.xml new file mode 100644 index 00000000..12d1ffee --- /dev/null +++ b/app/src/main/res/layout/transfer_connected_fragment.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_select_data_fragment.xml b/app/src/main/res/layout/transfer_select_data_fragment.xml new file mode 100644 index 00000000..a750e826 --- /dev/null +++ b/app/src/main/res/layout/transfer_select_data_fragment.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1b320e63..3a318552 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -48,6 +48,7 @@ ダウンロード ページ移動 現ページ番号: %1$d\nページ数: %2$d + 転送 hitomi.laに接続できません %1$dページへ移動 ダウンロード削除 @@ -160,4 +161,5 @@ ネットワーク ダウンロードデータベースを再構築 他の機器にデータを転送 + 他の機器へのデータ転送の進捗度を表示 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 583f3f15..4edcfafc 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -47,6 +47,7 @@ 다운로드 페이지 이동 현재 페이지: %1$d\n페이지 수: %2$d + 전송 hitomi.la에 연결할 수 없습니다 %1$d 페이지로 이동 다운로드 삭제 @@ -160,4 +161,5 @@ 네트워크 다운로드 데이터베이스 복구 다른 기기에 데이터 전송 + 다른 기기에 데이터 전송 시 상태 표시 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a60e8c4..87164ae8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -43,6 +43,9 @@ Update Shows update progress + Transfer + Shows progress of transferring data to another device + Unable to connect to hitomi.la Lock file corrupted! Please re-install Pupil