From a5d4cbfaecbd957675f4fbaad6d3fd47a96d4c86 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Thu, 11 Apr 2024 08:44:45 -0700 Subject: [PATCH 1/4] fix ellipsize --- .../quaver/pupil/services/DownloadService.kt | 1 - .../pupil/util/downloader/DownloadManager.kt | 5 +++- .../main/java/xyz/quaver/pupil/util/misc.kt | 29 +++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) 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 54bf595e..3f39eeb8 100644 --- a/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt +++ b/app/src/main/java/xyz/quaver/pupil/services/DownloadService.kt @@ -226,7 +226,6 @@ class DownloadService : Service() { } override fun onResponse(call: Call, response: Response) { - Log.d("PUPILD", "ONRESPONSE ${call.request().tag()}") val (galleryID, index, startId) = call.request().tag() as Tag val ext = call.request().url().encodedPath().split('.').last() 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 ed8f088f..c868c24e 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 @@ -28,7 +28,10 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import okhttp3.Call import xyz.quaver.io.FileX -import xyz.quaver.io.util.* +import xyz.quaver.io.util.deleteRecursively +import xyz.quaver.io.util.getChild +import xyz.quaver.io.util.readText +import xyz.quaver.io.util.writeText import xyz.quaver.pupil.client import xyz.quaver.pupil.services.DownloadService import xyz.quaver.pupil.util.Preferences 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 9e666680..117fed4a 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt @@ -125,11 +125,30 @@ suspend fun GalleryInfo.getRequestBuilders(): List { } } -fun String.ellipsize(n: Int): String = - if (this.length > n) - this.slice(0 until n) + "…" - else - this +fun byteCount(codePoint: Int): Int = when (codePoint) { + in 0 ..< 0x80 -> 1 + in 0x80 ..< 0x800 -> 2 + in 0x800 ..< 0x10000 -> 3 + in 0x10000 ..< 0x110000 -> 4 + else -> 0 +} + +fun String.ellipsize(n: Int): String = buildString { + var count = 0 + var index = 0 + val codePointLength = this.codePointCount(0, this.length) + + while (index < codePointLength) { + val next = this.codePointAt(index) + if (count + next > 124) { + append("…") + break + } + appendCodePoint(next) + count += next + index++ + } +} operator fun JsonElement.get(index: Int) = this.jsonArray[index] From 03b88c5b4b2ffda8df67327baac53ba034006b76 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Thu, 11 Apr 2024 07:53:38 -0700 Subject: [PATCH 2/4] wip --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 7 + .../pupil/adapters/TransferPeersAdapter.kt | 44 +++ .../receiver/WifiDirectBroadcastReceiver.kt | 59 ++++ .../xyz/quaver/pupil/ui/TransferActivity.kt | 294 ++++++++++++++++++ .../pupil/ui/fragment/SettingsFragment.kt | 9 + .../ui/fragment/TransferDirectionFragment.kt | 44 +++ .../ui/fragment/TransferPermissionFragment.kt | 38 +++ .../ui/fragment/TransferTargetFragment.kt | 69 ++++ .../TransferWaitForConnectionFragment.kt | 32 ++ app/src/main/res/drawable/arrow.xml | 13 + app/src/main/res/drawable/link.xml | 13 + app/src/main/res/drawable/round_button.xml | 13 + app/src/main/res/drawable/transfer_device.xml | 12 + app/src/main/res/drawable/transfer_ripple.xml | 9 + app/src/main/res/drawable/warning.xml | 13 + app/src/main/res/layout/transfer_activity.xml | 7 + .../layout/transfer_direction_fragment.xml | 89 ++++++ .../res/layout/transfer_peer_list_item.xml | 32 ++ .../layout/transfer_permission_fragment.xml | 38 +++ .../res/layout/transfer_target_fragment.xml | 58 ++++ .../transfer_wait_for_connection_fragment.xml | 54 ++++ app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/root_preferences.xml | 4 + 26 files changed, 956 insertions(+) create mode 100644 app/src/main/java/xyz/quaver/pupil/adapters/TransferPeersAdapter.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/receiver/WifiDirectBroadcastReceiver.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferDirectionFragment.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferPermissionFragment.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferTargetFragment.kt create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferWaitForConnectionFragment.kt create mode 100644 app/src/main/res/drawable/arrow.xml create mode 100644 app/src/main/res/drawable/link.xml create mode 100644 app/src/main/res/drawable/round_button.xml create mode 100644 app/src/main/res/drawable/transfer_device.xml create mode 100644 app/src/main/res/drawable/transfer_ripple.xml create mode 100644 app/src/main/res/drawable/warning.xml create mode 100644 app/src/main/res/layout/transfer_activity.xml create mode 100644 app/src/main/res/layout/transfer_direction_fragment.xml create mode 100644 app/src/main/res/layout/transfer_peer_list_item.xml create mode 100644 app/src/main/res/layout/transfer_permission_fragment.xml create mode 100644 app/src/main/res/layout/transfer_target_fragment.xml create mode 100644 app/src/main/res/layout/transfer_wait_for_connection_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index e87d894f..bceb8214 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -127,6 +127,8 @@ dependencies { implementation "ru.noties.markwon:core:3.1.0" + implementation "com.skyfishjy.ripplebackground:library:1.0.1" + implementation "org.jsoup:jsoup:1.14.3" implementation "xyz.quaver:documentfilex:0.7.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b07b068..640c0d6d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,12 @@ + + + + @@ -179,6 +185,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/adapters/TransferPeersAdapter.kt b/app/src/main/java/xyz/quaver/pupil/adapters/TransferPeersAdapter.kt new file mode 100644 index 00000000..4b95d64a --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/adapters/TransferPeersAdapter.kt @@ -0,0 +1,44 @@ +package xyz.quaver.pupil.adapters + +import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pDeviceList +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import androidx.recyclerview.widget.RecyclerView +import xyz.quaver.pupil.R +import xyz.quaver.pupil.databinding.TransferPeerListItemBinding + +class TransferPeersAdapter( + private val devices: Collection, + private val onDeviceSelected: (WifiP2pDevice) -> Unit +): RecyclerView.Adapter() { + + class ViewHolder(val binding: TransferPeerListItemBinding): RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = TransferPeerListItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val device = devices.elementAt(position) + + holder.binding.deviceName.text = device.deviceName + holder.binding.deviceAddress.text = device.deviceAddress + + holder.binding.root.setOnClickListener { + onDeviceSelected(device) + } + } + + override fun getItemCount(): Int { + return devices.size + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/receiver/WifiDirectBroadcastReceiver.kt b/app/src/main/java/xyz/quaver/pupil/receiver/WifiDirectBroadcastReceiver.kt new file mode 100644 index 00000000..55eebdd3 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/receiver/WifiDirectBroadcastReceiver.kt @@ -0,0 +1,59 @@ +package xyz.quaver.pupil.receiver + +import android.Manifest +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.wifi.p2p.WifiP2pManager +import android.os.Build +import android.os.Parcelable +import android.util.Log +import androidx.core.app.ActivityCompat +import xyz.quaver.pupil.ui.ErrorType +import xyz.quaver.pupil.ui.TransferStep +import xyz.quaver.pupil.ui.TransferViewModel + +private inline fun Intent.getParcelableExtraCompat(key: String): T? = when { + Build.VERSION.SDK_INT >= 33 -> getParcelableExtra(key, T::class.java) + else -> @Suppress("DEPRECATION") getParcelableExtra(key) as? T +} + +class WifiDirectBroadcastReceiver( + private val manager: WifiP2pManager, + private val channel: WifiP2pManager.Channel, + private val viewModel: TransferViewModel +): BroadcastReceiver() { + @SuppressLint("MissingPermission") + override fun onReceive(context: Context?, intent: Intent?) { + context!! + when (intent?.action) { + WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> { + val state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1) + viewModel.setWifiP2pEnabled(state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) + } + WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> { + manager.requestPeers(channel) { peers -> + viewModel.setPeers(peers) + } + } + WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> { + // Respond to new connection or disconnections + val networkInfo = intent.getParcelableExtraCompat(WifiP2pManager.EXTRA_NETWORK_INFO) + + if (networkInfo?.isConnected == true) { + manager.requestConnectionInfo(channel) { info -> + viewModel.setConnectionInfo(info) + } + } else { + viewModel.setConnectionInfo(null) + } + } + WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> { + // Respond to this device's wifi state changing + viewModel.setThisDevice(intent.getParcelableExtraCompat(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)) + } + } + } +} \ 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 new file mode 100644 index 00000000..50d61087 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt @@ -0,0 +1,294 @@ +package xyz.quaver.pupil.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.IntentFilter +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.net.wifi.WpsInfo +import android.net.wifi.p2p.WifiP2pConfig +import android.net.wifi.p2p.WifiP2pDevice +import android.net.wifi.p2p.WifiP2pDeviceList +import android.net.wifi.p2p.WifiP2pInfo +import android.net.wifi.p2p.WifiP2pManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.Bundle +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.fragment.app.commit +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import xyz.quaver.pupil.R +import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver +import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment +import xyz.quaver.pupil.ui.fragment.TransferPermissionFragment +import xyz.quaver.pupil.ui.fragment.TransferTargetFragment +import xyz.quaver.pupil.ui.fragment.TransferWaitForConnectionFragment +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +class TransferActivity : AppCompatActivity(R.layout.transfer_activity) { + + private val viewModel: TransferViewModel by viewModels() + + private val intentFilter = IntentFilter().apply { + addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) + } + + private lateinit var manager: WifiP2pManager + private lateinit var channel: WifiP2pManager.Channel + + private var receiver: BroadcastReceiver? = null + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + viewModel.setStep(TransferStep.TARGET) + } else { + viewModel.setStep(TransferStep.PERMISSION) + } + } + + private fun checkPermission(force: Boolean = false): Boolean { + val permissionRequired = if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) { + Manifest.permission.ACCESS_FINE_LOCATION + } else { + Manifest.permission.NEARBY_WIFI_DEVICES + } + + val permissionGranted = + ActivityCompat.checkSelfPermission(this, permissionRequired) == PackageManager.PERMISSION_GRANTED + + val shouldShowRationale = + ActivityCompat.shouldShowRequestPermissionRationale(this, permissionRequired) + + if (!permissionGranted) { + if (shouldShowRationale && force) { + viewModel.setStep(TransferStep.PERMISSION) + } else { + requestPermissionLauncher.launch(permissionRequired) + } + return false + } + + return true + } + + @SuppressLint("SourceLockedOrientationActivity") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar?.hide() + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + + manager = getSystemService(WIFI_P2P_SERVICE) as WifiP2pManager + channel = manager.initialize(this, mainLooper, null) + + viewModel.peerToConnect.observe(this) { peer -> + if (peer == null) { return@observe } + if (!checkPermission()) { return@observe } + + val config = WifiP2pConfig().apply { + deviceAddress = peer.deviceAddress + wps.setup = WpsInfo.PBC + } + + manager.connect(channel, config, object: WifiP2pManager.ActionListener { + override fun onSuccess() { + Log.d("PUPILD", "Connection successful") + } + + override fun onFailure(reason: Int) { + Log.d("PUPILD", "Connection failed: $reason") + viewModel.setPeers(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 -> + when (step) { + TransferStep.TARGET, + TransferStep.TARGET_FORCE -> { + if (!checkPermission(step == TransferStep.TARGET_FORCE)) { + return@collect + } + + manager.discoverPeers(channel, object: WifiP2pManager.ActionListener { + override fun onSuccess() { } + + override fun onFailure(reason: Int) { + } + }) + + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferTargetFragment()) + } + } + TransferStep.DIRECTION -> { + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferDirectionFragment()) + } + } + TransferStep.PERMISSION -> { + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferPermissionFragment()) + } + } + TransferStep.WAIT_FOR_CONNECTION -> { + if (!checkPermission()) { return@collect } + + runCatching { + suspendCoroutine { continuation -> + manager.removeGroup(channel, object: WifiP2pManager.ActionListener { + override fun onSuccess() { + continuation.resume(Unit) + } + + override fun onFailure(reason: Int) { + continuation.resume(Unit) + } + }) + } + + suspendCoroutine { continuation -> + manager.cancelConnect(channel, object: WifiP2pManager.ActionListener { + override fun onSuccess() { + continuation.resume(Unit) + } + + override fun onFailure(reason: Int) { + continuation.resume(Unit) + } + }) + } + + suspendCoroutine { continuation -> + manager.createGroup(channel, object: WifiP2pManager.ActionListener { + override fun onSuccess() { + continuation.resume(Unit) + } + + override fun onFailure(reason: Int) { + continuation.resumeWithException(Exception("Failed to create group $reason")) + } + }) + } + }.onFailure { + Log.e("PUPILD", "Failed to create group", it) + } + + supportFragmentManager.commit { + replace(R.id.fragment_container_view, TransferWaitForConnectionFragment()) + } + } + } + } + } + + } + + override fun onResume() { + super.onResume() + WifiDirectBroadcastReceiver(manager, channel, viewModel).also { + receiver = it + registerReceiver(it, intentFilter) + } + } + + override fun onPause() { + super.onPause() + receiver?.let { unregisterReceiver(it) } + receiver = null + } +} + +enum class TransferStep { + TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION +} + +enum class ErrorType { +} + +class TransferViewModel : ViewModel() { + private val _step: MutableStateFlow = MutableStateFlow(TransferStep.DIRECTION) + val step: StateFlow = _step + + private val _error = MutableLiveData(null) + val error: LiveData = _error + + private val _wifiP2pEnabled: MutableLiveData = MutableLiveData(false) + val wifiP2pEnabled: LiveData = _wifiP2pEnabled + + private val _thisDevice: MutableLiveData = MutableLiveData(null) + val thisDevice: LiveData = _thisDevice + + private val _peers: MutableLiveData = MutableLiveData(null) + val peers: LiveData = _peers + + private val _connectionInfo: MutableLiveData = MutableLiveData(null) + val connectionInfo: LiveData = _connectionInfo + + private val _peerToConnect: MutableLiveData = MutableLiveData(null) + val peerToConnect: LiveData = _peerToConnect + + fun setStep(step: TransferStep) { + _step.value = step + } + + fun setWifiP2pEnabled(enabled: Boolean) { + _wifiP2pEnabled.value = enabled + } + + fun setThisDevice(device: WifiP2pDevice?) { + _thisDevice.value = device + } + + fun setPeers(peers: WifiP2pDeviceList?) { + _peers.value = peers + } + + fun setConnectionInfo(info: WifiP2pInfo?) { + _connectionInfo.value = info + } + + fun setError(error: ErrorType?) { + _error.value = error + } + + fun connect(device: WifiP2pDevice) { + _peerToConnect.value = device + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt index 97706f8b..e1cd9a30 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/SettingsFragment.kt @@ -21,12 +21,14 @@ package xyz.quaver.pupil.ui.fragment import android.app.Activity import android.content.* import android.os.Bundle +import android.util.Log import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate import androidx.preference.* import com.google.android.gms.oss.licenses.OssLicensesMenuActivity import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.crashlytics.internal.model.CrashlyticsReport import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -40,6 +42,7 @@ import xyz.quaver.pupil.clientHolder import xyz.quaver.pupil.types.SendLogException import xyz.quaver.pupil.ui.LockActivity import xyz.quaver.pupil.ui.SettingsActivity +import xyz.quaver.pupil.ui.TransferActivity import xyz.quaver.pupil.ui.dialog.* import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.downloader.DownloadManager @@ -113,6 +116,9 @@ class SettingsFragment : ) Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } + "transfer_data" -> { + activity?.startActivity(Intent(activity, TransferActivity::class.java)) + } else -> return false } } @@ -300,6 +306,9 @@ class SettingsFragment : true } } + "transfer_data" -> { + onPreferenceClickListener = this@SettingsFragment + } } } diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferDirectionFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferDirectionFragment.kt new file mode 100644 index 00000000..27584eda --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferDirectionFragment.kt @@ -0,0 +1,44 @@ +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.R +import xyz.quaver.pupil.databinding.TransferDirectionFragmentBinding +import xyz.quaver.pupil.ui.TransferStep +import xyz.quaver.pupil.ui.TransferViewModel + +class TransferDirectionFragment : Fragment(R.layout.transfer_direction_fragment) { + + private var _binding: TransferDirectionFragmentBinding? = null + private val binding get() = _binding!! + + private val viewModel: TransferViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = TransferDirectionFragmentBinding.inflate(inflater, container, false) + + binding.inButton.setOnClickListener { + viewModel.setStep(TransferStep.TARGET) + } + + binding.outButton.setOnClickListener { + viewModel.setStep(TransferStep.WAIT_FOR_CONNECTION) + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferPermissionFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferPermissionFragment.kt new file mode 100644 index 00000000..e6155814 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferPermissionFragment.kt @@ -0,0 +1,38 @@ +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.TransferPermissionFragmentBinding +import xyz.quaver.pupil.ui.TransferStep +import xyz.quaver.pupil.ui.TransferViewModel + +class TransferPermissionFragment: Fragment() { + + private var _binding: TransferPermissionFragmentBinding? = null + private val binding get() = _binding!! + + private val viewModel: TransferViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = TransferPermissionFragmentBinding.inflate(inflater, container, false) + + binding.permissionsButton.setOnClickListener { + viewModel.setStep(TransferStep.TARGET_FORCE) + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferTargetFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferTargetFragment.kt new file mode 100644 index 00000000..70584dba --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferTargetFragment.kt @@ -0,0 +1,69 @@ +package xyz.quaver.pupil.ui.fragment + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import xyz.quaver.pupil.R +import xyz.quaver.pupil.adapters.TransferPeersAdapter +import xyz.quaver.pupil.databinding.TransferTargetFragmentBinding +import xyz.quaver.pupil.ui.TransferStep +import xyz.quaver.pupil.ui.TransferViewModel + +class TransferTargetFragment : Fragment() { + + private var _binding: TransferTargetFragmentBinding? = null + private val binding get() = _binding!! + + private val viewModel: TransferViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = TransferTargetFragmentBinding.inflate(inflater, container, false) + + viewModel.thisDevice.observe(viewLifecycleOwner) { device -> + if (device == null) { + return@observe + } + + if (device.status == 3) { + binding.ripple.startRippleAnimation() + binding.retryButton.visibility = View.INVISIBLE + } else { + binding.ripple.stopRippleAnimation() + binding.retryButton.visibility = View.VISIBLE + } + } + + viewModel.peers.observe(viewLifecycleOwner) { peers -> + if (peers == null) { + return@observe + } + + binding.deviceList.adapter = TransferPeersAdapter(peers.deviceList) { + viewModel.connect(it) + } + } + + binding.ripple.startRippleAnimation() + + binding.retryButton.setOnClickListener { + viewModel.setStep(TransferStep.TARGET) + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferWaitForConnectionFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferWaitForConnectionFragment.kt new file mode 100644 index 00000000..41f81cc4 --- /dev/null +++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferWaitForConnectionFragment.kt @@ -0,0 +1,32 @@ +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 xyz.quaver.pupil.databinding.TransferWaitForConnectionFragmentBinding + +class TransferWaitForConnectionFragment : Fragment() { + + private var _binding: TransferWaitForConnectionFragmentBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = TransferWaitForConnectionFragmentBinding.inflate(layoutInflater) + + binding.ripple.startRippleAnimation() + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow.xml b/app/src/main/res/drawable/arrow.xml new file mode 100644 index 00000000..f4eda35b --- /dev/null +++ b/app/src/main/res/drawable/arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/link.xml b/app/src/main/res/drawable/link.xml new file mode 100644 index 00000000..8e1158ad --- /dev/null +++ b/app/src/main/res/drawable/link.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/round_button.xml b/app/src/main/res/drawable/round_button.xml new file mode 100644 index 00000000..b167415e --- /dev/null +++ b/app/src/main/res/drawable/round_button.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transfer_device.xml b/app/src/main/res/drawable/transfer_device.xml new file mode 100644 index 00000000..f202c9ab --- /dev/null +++ b/app/src/main/res/drawable/transfer_device.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transfer_ripple.xml b/app/src/main/res/drawable/transfer_ripple.xml new file mode 100644 index 00000000..db299b54 --- /dev/null +++ b/app/src/main/res/drawable/transfer_ripple.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/warning.xml b/app/src/main/res/drawable/warning.xml new file mode 100644 index 00000000..c5cbb208 --- /dev/null +++ b/app/src/main/res/drawable/warning.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/transfer_activity.xml b/app/src/main/res/layout/transfer_activity.xml new file mode 100644 index 00000000..9ddc1a47 --- /dev/null +++ b/app/src/main/res/layout/transfer_activity.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_direction_fragment.xml b/app/src/main/res/layout/transfer_direction_fragment.xml new file mode 100644 index 00000000..3089f3fc --- /dev/null +++ b/app/src/main/res/layout/transfer_direction_fragment.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_peer_list_item.xml b/app/src/main/res/layout/transfer_peer_list_item.xml new file mode 100644 index 00000000..6f576f5c --- /dev/null +++ b/app/src/main/res/layout/transfer_peer_list_item.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_permission_fragment.xml b/app/src/main/res/layout/transfer_permission_fragment.xml new file mode 100644 index 00000000..7f6ceb72 --- /dev/null +++ b/app/src/main/res/layout/transfer_permission_fragment.xml @@ -0,0 +1,38 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_target_fragment.xml b/app/src/main/res/layout/transfer_target_fragment.xml new file mode 100644 index 00000000..25a94a12 --- /dev/null +++ b/app/src/main/res/layout/transfer_target_fragment.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/transfer_wait_for_connection_fragment.xml b/app/src/main/res/layout/transfer_wait_for_connection_fragment.xml new file mode 100644 index 00000000..8fdd34e2 --- /dev/null +++ b/app/src/main/res/layout/transfer_wait_for_connection_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ea44ad2f..1b320e63 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -159,4 +159,5 @@ アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか? ネットワーク ダウンロードデータベースを再構築 + 他の機器にデータを転送 \ 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 8554abb8..583f3f15 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -159,4 +159,5 @@ 안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까? 네트워크 다운로드 데이터베이스 복구 + 다른 기기에 데이터 전송 \ 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 c2cc2bbf..3a60e8c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -174,6 +174,7 @@ Hide image from gallery Low quality images Load low quality images to improve load speed and data usage + Transfer data to another device diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index de11f055..9446e355 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -52,6 +52,10 @@ app:defaultValue="8" app:useSimpleSummaryProvider="true"/> + + 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 3/4] 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 From 68ec919ae43bc8304f515e5e35d9ab6ffe175835 Mon Sep 17 00:00:00 2001 From: tom5079 <7948651+tom5079@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:25:36 -0700 Subject: [PATCH 4/4] fixed downloading galleris with long title, [wip] transfer data --- app/build.gradle | 2 +- app/release/output-metadata.json | 2 +- .../quaver/pupil/services/DownloadService.kt | 6 +- .../pupil/services/TransferClientService.kt | 41 ++++++-- .../quaver/pupil/services/TransferProtocol.kt | 94 +++++++++++++++++++ .../pupil/services/TransferServerService.kt | 29 +++++- .../xyz/quaver/pupil/ui/TransferActivity.kt | 51 +++++++--- .../ui/fragment/TransferSelectDataFragment.kt | 2 +- .../pupil/util/downloader/DownloadManager.kt | 1 + .../main/java/xyz/quaver/pupil/util/misc.kt | 13 ++- app/src/main/res/xml/root_preferences.xml | 6 +- 11 files changed, 209 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/xyz/quaver/pupil/services/TransferProtocol.kt 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"/> - + + +