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] 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"/>
+
+