This commit is contained in:
tom5079
2024-04-11 07:53:38 -07:00
parent a5d4cbfaec
commit 03b88c5b4b
26 changed files with 956 additions and 0 deletions

View File

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

View File

@@ -13,6 +13,12 @@
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32"
tools:ignore="CoarseFineLocation" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
@@ -179,6 +185,7 @@
</intent-filter>
</activity>
<activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" />
<activity android:name=".ui.TransferActivity" />
</application>
</manifest>

View File

@@ -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<WifiP2pDevice>,
private val onDeviceSelected: (WifiP2pDevice) -> Unit
): RecyclerView.Adapter<TransferPeersAdapter.ViewHolder>() {
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
}
}

View File

@@ -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 <reified T : Parcelable> 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<android.net.NetworkInfo>(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))
}
}
}
}

View File

@@ -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<TransferStep> = MutableStateFlow(TransferStep.DIRECTION)
val step: StateFlow<TransferStep> = _step
private val _error = MutableLiveData<ErrorType?>(null)
val error: LiveData<ErrorType?> = _error
private val _wifiP2pEnabled: MutableLiveData<Boolean> = MutableLiveData(false)
val wifiP2pEnabled: LiveData<Boolean> = _wifiP2pEnabled
private val _thisDevice: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
val thisDevice: LiveData<WifiP2pDevice?> = _thisDevice
private val _peers: MutableLiveData<WifiP2pDeviceList?> = MutableLiveData(null)
val peers: LiveData<WifiP2pDeviceList?> = _peers
private val _connectionInfo: MutableLiveData<WifiP2pInfo?> = MutableLiveData(null)
val connectionInfo: LiveData<WifiP2pInfo?> = _connectionInfo
private val _peerToConnect: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
val peerToConnect: LiveData<WifiP2pDevice?> = _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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.5,10.5 L12,3m0,0 l7.5,7.5M12,3v18"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorPrimaryDark"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.19,8.688a4.5,4.5 0,0 1,1.242 7.244l-4.5,4.5a4.5,4.5 0,0 1,-6.364 -6.364l1.757,-1.757m13.35,-0.622 l1.757,-1.757a4.5,4.5 0,0 0,-6.364 -6.364l-4.5,4.5a4.5,4.5 0,0 0,1.242 7.244"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorPrimaryDark"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false">
<shape android:shape="oval">
<solid android:color="#dddddd"/>
</shape>
</item>
<item android:state_pressed="true">
<shape android:shape="oval">
<solid android:color="#888888"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:bottom="-8dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="32dp"
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp" />
<stroke android:width="4dp" android:color="#444444"/>
</shape>
</item>
</layer-list>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="#9d9d9d">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="#9d9d9d" />
<corners android:radius="16dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,9v3.75m-9.303,3.376c-0.866,1.5 0.217,3.374 1.948,3.374h14.71c1.73,0 2.813,-1.874 1.948,-3.374L13.949,3.378c-0.866,-1.5 -3.032,-1.5 -3.898,0L2.697,16.126ZM12,15.75h0.007v0.008H12v-0.008Z"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="@color/colorAccent"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="xyz.quaver.pupil.ui.fragment.TransferDirectionFragment" />

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="Transfer your data"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/device"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginHorizontal="64dp"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/transfer_device"/>
<LinearLayout
android:id="@+id/out_button"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:background="@drawable/transfer_ripple"
app:layout_constraintBottom_toTopOf="@id/device"
app:layout_constraintLeft_toLeftOf="@id/device"
app:layout_constraintRight_toRightOf="@id/device"
android:layout_marginBottom="32dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/out_arrow"
android:layout_width="48dp"
android:layout_height="48dp"
app:srcCompat="@drawable/arrow"
android:layout_marginBottom="4dp" />
<TextView
android:id="@+id/out_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send data"
android:textStyle="bold"
android:layout_marginTop="4dp"/>
</LinearLayout>
<LinearLayout
android:id="@+id/in_button"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical"
android:gravity="center"
android:clickable="true"
android:focusable="true"
android:background="@drawable/transfer_ripple"
app:layout_constraintTop_toTopOf="@id/device"
app:layout_constraintLeft_toLeftOf="@id/device"
app:layout_constraintRight_toRightOf="@id/device"
android:layout_marginTop="32dp">
<TextView
android:id="@+id/in_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Receive data"
android:textStyle="bold"
android:layout_marginBottom="4dp"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/in_arrow"
android:layout_width="48dp"
android:layout_height="48dp"
app:srcCompat="@drawable/arrow"
android:layout_marginTop="4dp"
android:rotation="180"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:padding="16dp">
<TextView
android:id="@+id/device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Gonsales Vorecek's Galaxy S22 Ultra"
android:textStyle="bold"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/device_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="26:7D:2D:3A:4F:5E"
android:textSize="16sp"
app:layout_constraintTop_toBottomOf="@id/device_name"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="4dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="48dp"
app:srcCompat="@drawable/warning"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Permissions Missing"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/icon" />
<com.google.android.material.button.MaterialButton
android:id="@+id/permissions_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Grant Permissions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_marginHorizontal="36dp"
android:layout_marginVertical="64dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="48dp"
app:srcCompat="@drawable/link"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Connect to your device"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/icon" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/device_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_marginHorizontal="32dp"
android:layout_marginVertical="32dp"
app:layout_constraintTop_toBottomOf="@id/textView"
app:layout_constraintBottom_toBottomOf="parent" />
<com.skyfishjy.library.RippleBackground
android:id="@+id/ripple"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rb_color="@color/colorPrimaryDark"
app:rb_radius="32dp"
app:rb_type="strokeRipple"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Retry"
style="@style/Widget.AppCompat.Button.Borderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:visibility="invisible"
android:layout_margin="32dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="48dp"
app:srcCompat="@drawable/link"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Wait for connection"
android:textAlignment="center"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintTop_toBottomOf="@id/icon" />
<com.skyfishjy.library.RippleBackground
android:id="@+id/ripple"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1"
app:layout_constraintTop_toTopOf="@id/barrier"
app:layout_constraintBottom_toBottomOf="@id/barrier"
app:rb_color="@color/colorPrimaryDark"
app:rb_radius="32dp"
app:rb_type="strokeRipple"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="device" />
<View
android:id="@+id/device"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginHorizontal="64dp"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/transfer_device"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -159,4 +159,5 @@
<string name="unaccessible_download_folder">アンドロイド11以上では外部からのアプリ内部空間接近が不可能です。ダウンロードフォルダを変更しますか</string>
<string name="settings_networking">ネットワーク</string>
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
<string name="settings_transfer_data">他の機器にデータを転送</string>
</resources>

View File

@@ -159,4 +159,5 @@
<string name="unaccessible_download_folder">안드로이드 11 이상에서는 외부에서 현재 다운로드 폴더에 접근할 수 없습니다. 변경하시겠습니까?</string>
<string name="settings_networking">네트워크</string>
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
<string name="settings_transfer_data">다른 기기에 데이터 전송</string>
</resources>

View File

@@ -174,6 +174,7 @@
<string name="settings_nomedia_title">Hide image from gallery</string>
<string name="settings_low_quality">Low quality images</string>
<string name="settings_low_quality_summary">Load low quality images to improve load speed and data usage</string>
<string name="settings_transfer_data">Transfer data to another device</string>
<!-- SETTINGS/APP LOCK -->

View File

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