wip
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
294
app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt
Normal file
294
app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
13
app/src/main/res/drawable/arrow.xml
Normal file
13
app/src/main/res/drawable/arrow.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/link.xml
Normal file
13
app/src/main/res/drawable/link.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/round_button.xml
Normal file
13
app/src/main/res/drawable/round_button.xml
Normal 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>
|
||||
12
app/src/main/res/drawable/transfer_device.xml
Normal file
12
app/src/main/res/drawable/transfer_device.xml
Normal 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>
|
||||
9
app/src/main/res/drawable/transfer_ripple.xml
Normal file
9
app/src/main/res/drawable/transfer_ripple.xml
Normal 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>
|
||||
13
app/src/main/res/drawable/warning.xml
Normal file
13
app/src/main/res/drawable/warning.xml
Normal 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>
|
||||
7
app/src/main/res/layout/transfer_activity.xml
Normal file
7
app/src/main/res/layout/transfer_activity.xml
Normal 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" />
|
||||
89
app/src/main/res/layout/transfer_direction_fragment.xml
Normal file
89
app/src/main/res/layout/transfer_direction_fragment.xml
Normal 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>
|
||||
32
app/src/main/res/layout/transfer_peer_list_item.xml
Normal file
32
app/src/main/res/layout/transfer_peer_list_item.xml
Normal 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>
|
||||
38
app/src/main/res/layout/transfer_permission_fragment.xml
Normal file
38
app/src/main/res/layout/transfer_permission_fragment.xml
Normal 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>
|
||||
58
app/src/main/res/layout/transfer_target_fragment.xml
Normal file
58
app/src/main/res/layout/transfer_target_fragment.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user