wip
This commit is contained in:
@@ -82,13 +82,14 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
|
||||
|
||||
|
||||
implementation "androidx.core:core-ktx:1.12.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
@@ -116,6 +117,7 @@ dependencies {
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
|
||||
implementation "io.ktor:ktor-network:2.3.10"
|
||||
|
||||
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="23"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" android:usesPermissionFlags="neverForLocation"
|
||||
tools:targetApi="s" />
|
||||
@@ -54,7 +54,15 @@
|
||||
|
||||
<service android:name=".services.DownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse" />
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".services.TransferClientService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name=".services.TransferServerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.UpdateBroadcastReceiver"
|
||||
|
||||
@@ -236,6 +236,13 @@ class Pupil : Application() {
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("transfer", getString(R.string.channel_transfer), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_transfer_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
}
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(when (Preferences.get<Boolean>("dark_mode")) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package xyz.quaver.pupil.services
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import io.ktor.network.selector.SelectorManager
|
||||
import io.ktor.network.sockets.aSocket
|
||||
import io.ktor.network.sockets.openReadChannel
|
||||
import io.ktor.network.sockets.openWriteChannel
|
||||
import io.ktor.utils.io.writeStringUtf8
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class TransferClientService : Service() {
|
||||
private val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
private val channel = Channel<String>()
|
||||
private var job: Job? = null
|
||||
|
||||
private fun startForeground() = runCatching {
|
||||
val notification = NotificationCompat.Builder(this, "transfer")
|
||||
.setContentTitle("Pupil")
|
||||
.setContentText("Transfer server is running")
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.build()
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
1,
|
||||
notification,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else 0
|
||||
)
|
||||
}
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val address = intent?.getStringExtra("address") ?: run {
|
||||
stopSelf(startId)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
startForeground()
|
||||
|
||||
Log.d("PUPILD", "Starting service with address $address")
|
||||
|
||||
job?.cancel()
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
Log.d("PUPILD", "Connecting to $address")
|
||||
|
||||
val socket = aSocket(selectorManager).tcp().connect(address, 12221)
|
||||
|
||||
Log.d("PUPILD", "Connected to $address")
|
||||
|
||||
val readChannel = socket.openReadChannel()
|
||||
val writeChannel = socket.openWriteChannel(autoFlush = true)
|
||||
|
||||
runCatching {
|
||||
while (true) {
|
||||
val message = channel.receive()
|
||||
Log.d("PUPILD", "Sending message $message!")
|
||||
writeChannel.writeStringUtf8(message)
|
||||
Log.d("PUPILD", readChannel.readUTF8Line(4).toString())
|
||||
}
|
||||
}.onFailure {
|
||||
socket.close()
|
||||
stopSelf(startId)
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job?.cancel()
|
||||
}
|
||||
|
||||
inner class Binder: android.os.Binder() {
|
||||
fun sendMessage(message: String) {
|
||||
Log.d("PUPILD", "Sending message $message")
|
||||
channel.trySendBlocking(message + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder = binder
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package xyz.quaver.pupil.services
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import io.ktor.network.selector.SelectorManager
|
||||
import io.ktor.network.sockets.ServerSocket
|
||||
import io.ktor.network.sockets.Socket
|
||||
import io.ktor.network.sockets.aSocket
|
||||
import io.ktor.network.sockets.openReadChannel
|
||||
import io.ktor.network.sockets.openWriteChannel
|
||||
import io.ktor.utils.io.writeStringUtf8
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class TransferServerService : Service() {
|
||||
private val selectorManager = SelectorManager(Dispatchers.IO)
|
||||
private var serverSocket: ServerSocket? = null
|
||||
private val job = Job()
|
||||
|
||||
private fun startForeground() = runCatching {
|
||||
val notification = NotificationCompat.Builder(this, "transfer")
|
||||
.setContentTitle("Pupil")
|
||||
.setContentText("Transfer server is running")
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.build()
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
1,
|
||||
notification,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
} else 0
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun handleConnection(socket: Socket) {
|
||||
val readChannel = socket.openReadChannel()
|
||||
val writeChannel = socket.openWriteChannel(autoFlush = true)
|
||||
|
||||
runCatching {
|
||||
while (true) {
|
||||
if (readChannel.readUTF8Line(8) == "ping") {
|
||||
writeChannel.writeStringUtf8("pong\n")
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val address = intent?.getStringExtra("address") ?: run {
|
||||
stopSelf(startId)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
if (serverSocket != null) {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
startForeground()
|
||||
|
||||
val serverSocket = aSocket(selectorManager).tcp().bind(address, 12221).also {
|
||||
this@TransferServerService.serverSocket = it
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO + job).launch {
|
||||
while (true) {
|
||||
Log.d("PUPILD", "Waiting for connection")
|
||||
val socket = serverSocket.accept()
|
||||
Log.d("PUPILD", "Accepted connection from ${socket.remoteAddress}")
|
||||
launch { handleConnection(socket) }
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
serverSocket?.close()
|
||||
}
|
||||
|
||||
inner class Binder: android.os.Binder() {
|
||||
fun getService() = this@TransferServerService
|
||||
}
|
||||
|
||||
private val binder = Binder()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder = binder
|
||||
}
|
||||
@@ -3,7 +3,10 @@ package xyz.quaver.pupil.ui
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.wifi.WpsInfo
|
||||
@@ -15,11 +18,13 @@ import android.net.wifi.p2p.WifiP2pManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LiveData
|
||||
@@ -27,13 +32,23 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver
|
||||
import xyz.quaver.pupil.services.TransferClientService
|
||||
import xyz.quaver.pupil.services.TransferServerService
|
||||
import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment
|
||||
import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment
|
||||
import xyz.quaver.pupil.ui.fragment.TransferPermissionFragment
|
||||
import xyz.quaver.pupil.ui.fragment.TransferSelectDataFragment
|
||||
import xyz.quaver.pupil.ui.fragment.TransferTargetFragment
|
||||
import xyz.quaver.pupil.ui.fragment.TransferWaitForConnectionFragment
|
||||
import kotlin.coroutines.resume
|
||||
@@ -66,6 +81,18 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
}
|
||||
}
|
||||
|
||||
private var serviceBinder: TransferClientService.Binder? = null
|
||||
|
||||
private val serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
serviceBinder = service as TransferClientService.Binder
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
serviceBinder = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission(force: Boolean = false): Boolean {
|
||||
val permissionRequired = if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) {
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
@@ -110,40 +137,25 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
}
|
||||
|
||||
manager.connect(channel, config, object: WifiP2pManager.ActionListener {
|
||||
override fun onSuccess() {
|
||||
Log.d("PUPILD", "Connection successful")
|
||||
}
|
||||
override fun onSuccess() { }
|
||||
|
||||
override fun onFailure(reason: Int) {
|
||||
Log.d("PUPILD", "Connection failed: $reason")
|
||||
viewModel.setPeers(null)
|
||||
viewModel.connect(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.connectionInfo.observe(this) { info ->
|
||||
if (info == null) { return@observe }
|
||||
|
||||
if (info.groupFormed && info.isGroupOwner) {
|
||||
// Do something
|
||||
Log.d("PUPILD", "Group formed and is group owner")
|
||||
Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}")
|
||||
} else if (info.groupFormed) {
|
||||
// Do something
|
||||
Log.d("PUPILD", "Group formed")
|
||||
Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}")
|
||||
Log.d("PUPILD", "Local IP: ${info.groupOwnerAddress.hostAddress}")
|
||||
Log.d("PUPILD", "Is group owner: ${info.isGroupOwner}")
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { step ->
|
||||
viewModel.messageQueue.consumeEach {
|
||||
serviceBinder?.sendMessage(it)
|
||||
}
|
||||
|
||||
viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).cancellable().collect step@{ step ->
|
||||
Log.d("PUPILD", "Step: $step")
|
||||
when (step) {
|
||||
TransferStep.TARGET,
|
||||
TransferStep.TARGET_FORCE -> {
|
||||
if (!checkPermission(step == TransferStep.TARGET_FORCE)) {
|
||||
return@collect
|
||||
return@step
|
||||
}
|
||||
|
||||
manager.discoverPeers(channel, object: WifiP2pManager.ActionListener {
|
||||
@@ -156,6 +168,17 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container_view, TransferTargetFragment())
|
||||
}
|
||||
|
||||
val hostAddress = viewModel.connectionInfo.filterNotNull().first {
|
||||
it.groupFormed
|
||||
}.groupOwnerAddress.hostAddress
|
||||
|
||||
val intent = Intent(this@TransferActivity, TransferClientService::class.java).also {
|
||||
it.putExtra("address", hostAddress)
|
||||
}
|
||||
bindService(intent, serviceConnection, BIND_AUTO_CREATE)
|
||||
|
||||
viewModel.setStep(TransferStep.SELECT_DATA)
|
||||
}
|
||||
TransferStep.DIRECTION -> {
|
||||
supportFragmentManager.commit {
|
||||
@@ -168,7 +191,8 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
}
|
||||
}
|
||||
TransferStep.WAIT_FOR_CONNECTION -> {
|
||||
if (!checkPermission()) { return@collect }
|
||||
Log.d("PUPILD", "wait for connection")
|
||||
if (!checkPermission()) { return@step }
|
||||
|
||||
runCatching {
|
||||
suspendCoroutine { continuation ->
|
||||
@@ -206,6 +230,21 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
|
||||
}
|
||||
|
||||
val address = viewModel.connectionInfo.filterNotNull().first {
|
||||
it.groupFormed && it.isGroupOwner
|
||||
}.groupOwnerAddress.hostAddress
|
||||
|
||||
val intent = Intent(this@TransferActivity, TransferServerService::class.java).also {
|
||||
it.putExtra("address", address)
|
||||
}
|
||||
ContextCompat.startForegroundService(this@TransferActivity, intent)
|
||||
|
||||
viewModel.setStep(TransferStep.CONNECTED)
|
||||
}.onFailure {
|
||||
Log.e("PUPILD", "Failed to create group", it)
|
||||
}
|
||||
@@ -214,6 +253,16 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
|
||||
}
|
||||
}
|
||||
TransferStep.CONNECTED -> {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container_view, TransferConnectedFragment())
|
||||
}
|
||||
}
|
||||
TransferStep.SELECT_DATA -> {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container_view, TransferSelectDataFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +285,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
|
||||
}
|
||||
|
||||
enum class TransferStep {
|
||||
TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION
|
||||
TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION, CONNECTED, SELECT_DATA
|
||||
}
|
||||
|
||||
enum class ErrorType {
|
||||
@@ -258,13 +307,16 @@ class TransferViewModel : ViewModel() {
|
||||
private val _peers: MutableLiveData<WifiP2pDeviceList?> = MutableLiveData(null)
|
||||
val peers: LiveData<WifiP2pDeviceList?> = _peers
|
||||
|
||||
private val _connectionInfo: MutableLiveData<WifiP2pInfo?> = MutableLiveData(null)
|
||||
val connectionInfo: LiveData<WifiP2pInfo?> = _connectionInfo
|
||||
private val _connectionInfo: MutableStateFlow<WifiP2pInfo?> = MutableStateFlow(null)
|
||||
val connectionInfo: StateFlow<WifiP2pInfo?> = _connectionInfo
|
||||
|
||||
private val _peerToConnect: MutableLiveData<WifiP2pDevice?> = MutableLiveData(null)
|
||||
val peerToConnect: LiveData<WifiP2pDevice?> = _peerToConnect
|
||||
|
||||
val messageQueue: Channel<String> = Channel()
|
||||
|
||||
fun setStep(step: TransferStep) {
|
||||
Log.d("PUPILD", "Set step: $step")
|
||||
_step.value = step
|
||||
}
|
||||
|
||||
@@ -288,7 +340,11 @@ class TransferViewModel : ViewModel() {
|
||||
_error.value = error
|
||||
}
|
||||
|
||||
fun connect(device: WifiP2pDevice) {
|
||||
fun connect(device: WifiP2pDevice?) {
|
||||
_peerToConnect.value = device
|
||||
}
|
||||
|
||||
fun ping() {
|
||||
messageQueue.trySend("ping")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class TransferConnectedFragment: Fragment(R.layout.transfer_connected_fragment) {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import xyz.quaver.pupil.databinding.TransferSelectDataFragmentBinding
|
||||
import xyz.quaver.pupil.ui.TransferViewModel
|
||||
|
||||
class TransferSelectDataFragment: Fragment() {
|
||||
|
||||
private var _binding: TransferSelectDataFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val viewModel: TransferViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
_binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.checkAll.setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.ping()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
13
app/src/main/res/drawable/check.xml
Normal file
13
app/src/main/res/drawable/check.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="M9,12.75 L11.25,15 15,9.75M21,12a9,9 0,1 1,-18 0,9 9,0 0,1 18,0Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/colorPrimaryDark"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/download.xml
Normal file
13
app/src/main/res/drawable/download.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="M3,16.5v2.25A2.25,2.25 0,0 0,5.25 21h13.5A2.25,2.25 0,0 0,21 18.75V16.5M16.5,12 L12,16.5m0,0L7.5,12m4.5,4.5V3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/material_blue_700"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/heart.xml
Normal file
13
app/src/main/res/drawable/heart.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="M21,8.25c0,-2.485 -2.099,-4.5 -4.688,-4.5 -1.935,0 -3.597,1.126 -4.312,2.733 -0.715,-1.607 -2.377,-2.733 -4.313,-2.733C5.1,3.75 3,5.765 3,8.25c0,7.22 9,12 9,12s9,-4.78 9,-12Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/material_pink_600"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/history_rounded.xml
Normal file
9
app/src/main/res/drawable/history_rounded.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@color/material_orange_500"
|
||||
android:pathData="M477,840q-142,0 -243.5,-95.5T121,509q-1,-12 7.5,-21t21.5,-9q12,0 20.5,8.5T181,509q11,115 95,193t201,78q127,0 215,-89t88,-216q0,-124 -89,-209.5T477,180q-68,0 -127.5,31T246,293h75q13,0 21.5,8.5T351,323q0,13 -8.5,21.5T321,353L172,353q-13,0 -21.5,-8.5T142,323v-148q0,-13 8.5,-21.5T172,145q13,0 21.5,8.5T202,175v76q52,-61 123.5,-96T477,120q75,0 141,28t115.5,76.5Q783,273 811.5,338T840,478q0,75 -28.5,141t-78,115Q684,783 618,811.5T477,840ZM511,466 L626,579q9,9 9,21.5t-9,21.5q-9,9 -21,9t-21,-9L460,500q-5,-5 -7,-10.5t-2,-11.5v-171q0,-13 8.5,-21.5T481,277q13,0 21.5,8.5T511,307v159Z"/>
|
||||
</vector>
|
||||
28
app/src/main/res/layout/transfer_connected_fragment.xml
Normal file
28
app/src/main/res/layout/transfer_connected_fragment.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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/check"
|
||||
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="Device connected"
|
||||
android:textAlignment="center"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/icon" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
196
app/src/main/res/layout/transfer_select_data_fragment.xml
Normal file
196
app/src/main/res/layout/transfer_select_data_fragment.xml
Normal file
@@ -0,0 +1,196 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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/check"
|
||||
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="Choose data to transfer"
|
||||
android:textAlignment="center"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/icon" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/textView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/check_all_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="All"
|
||||
app:layout_constraintTop_toBottomOf="@id/check_all"
|
||||
app:layout_constraintLeft_toLeftOf="@id/check_all"
|
||||
app:layout_constraintRight_toRightOf="@id/check_all"
|
||||
android:layout_marginTop="-8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selected_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:text="3 items selected"
|
||||
android:textStyle="bold"
|
||||
android:textSize="24sp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintTop_toTopOf="@id/check_all"
|
||||
app:layout_constraintBottom_toTopOf="@id/selected_size"
|
||||
app:layout_constraintStart_toEndOf="@id/check_all" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selected_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="37.8 GB / About 28 minutes"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintTop_toTopOf="@id/check_all_label"
|
||||
app:layout_constraintBottom_toBottomOf="@id/check_all_label"
|
||||
app:layout_constraintStart_toStartOf="@id/selected_count" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?android:attr/dividerHorizontal"
|
||||
app:layout_constraintTop_toBottomOf="@id/check_all_label"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:layout_marginHorizontal="16dp"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_favorites"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/favorites_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/heart"
|
||||
app:layout_constraintStart_toEndOf="@id/check_favorites"
|
||||
app:layout_constraintTop_toTopOf="@id/check_favorites"
|
||||
app:layout_constraintBottom_toBottomOf="@id/check_favorites"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/favorites_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Favorites"
|
||||
android:textSize="28sp"
|
||||
app:layout_constraintTop_toTopOf="@id/favorites_icon"
|
||||
app:layout_constraintBottom_toTopOf="@id/favorites_count"
|
||||
app:layout_constraintStart_toEndOf="@id/favorites_icon"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/favorites_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="275 items"
|
||||
app:layout_constraintTop_toBottomOf="@id/favorites_label"
|
||||
app:layout_constraintBottom_toBottomOf="@id/favorites_icon"
|
||||
app:layout_constraintStart_toStartOf="@id/favorites_label" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_history"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/check_favorites"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/history_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/history_rounded"
|
||||
app:layout_constraintStart_toEndOf="@id/check_history"
|
||||
app:layout_constraintTop_toTopOf="@id/check_history"
|
||||
app:layout_constraintBottom_toBottomOf="@id/check_history"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="History"
|
||||
android:textSize="28sp"
|
||||
app:layout_constraintTop_toTopOf="@id/history_icon"
|
||||
app:layout_constraintBottom_toTopOf="@id/history_count"
|
||||
app:layout_constraintStart_toEndOf="@id/history_icon"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="2375 items"
|
||||
app:layout_constraintTop_toBottomOf="@id/history_label"
|
||||
app:layout_constraintBottom_toBottomOf="@id/history_icon"
|
||||
app:layout_constraintStart_toStartOf="@id/history_label" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/check_downloads"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/check_history"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="32dp"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/downloads_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="16dp"
|
||||
app:srcCompat="@drawable/download"
|
||||
app:layout_constraintStart_toEndOf="@id/check_downloads"
|
||||
app:layout_constraintTop_toTopOf="@id/check_downloads"
|
||||
app:layout_constraintBottom_toBottomOf="@id/check_downloads"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloads_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Downloads"
|
||||
android:textSize="28sp"
|
||||
app:layout_constraintTop_toTopOf="@id/downloads_icon"
|
||||
app:layout_constraintBottom_toTopOf="@id/downloads_count"
|
||||
app:layout_constraintStart_toEndOf="@id/downloads_icon"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloads_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="881 items"
|
||||
app:layout_constraintTop_toBottomOf="@id/downloads_label"
|
||||
app:layout_constraintBottom_toBottomOf="@id/downloads_icon"
|
||||
app:layout_constraintStart_toStartOf="@id/downloads_label" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -48,6 +48,7 @@
|
||||
<string name="main_drawer_downloads">ダウンロード</string>
|
||||
<string name="main_jump_title">ページ移動</string>
|
||||
<string name="main_jump_message">現ページ番号: %1$d\nページ数: %2$d</string>
|
||||
<string name="channel_transfer">転送</string>
|
||||
<string name="unable_to_connect">hitomi.laに接続できません</string>
|
||||
<string name="main_move_to_page">%1$dページへ移動</string>
|
||||
<string name="settings_clear_downloads">ダウンロード削除</string>
|
||||
@@ -160,4 +161,5 @@
|
||||
<string name="settings_networking">ネットワーク</string>
|
||||
<string name="settings_recover_downloads">ダウンロードデータベースを再構築</string>
|
||||
<string name="settings_transfer_data">他の機器にデータを転送</string>
|
||||
<string name="channel_transfer_description">他の機器へのデータ転送の進捗度を表示</string>
|
||||
</resources>
|
||||
@@ -47,6 +47,7 @@
|
||||
<string name="main_drawer_downloads">다운로드</string>
|
||||
<string name="main_jump_title">페이지 이동</string>
|
||||
<string name="main_jump_message">현재 페이지: %1$d\n페이지 수: %2$d</string>
|
||||
<string name="channel_transfer">전송</string>
|
||||
<string name="unable_to_connect">hitomi.la에 연결할 수 없습니다</string>
|
||||
<string name="main_move_to_page">%1$d 페이지로 이동</string>
|
||||
<string name="settings_clear_downloads">다운로드 삭제</string>
|
||||
@@ -160,4 +161,5 @@
|
||||
<string name="settings_networking">네트워크</string>
|
||||
<string name="settings_recover_downloads">다운로드 데이터베이스 복구</string>
|
||||
<string name="settings_transfer_data">다른 기기에 데이터 전송</string>
|
||||
<string name="channel_transfer_description">다른 기기에 데이터 전송 시 상태 표시</string>
|
||||
</resources>
|
||||
@@ -43,6 +43,9 @@
|
||||
<string name="channel_update">Update</string>
|
||||
<string name="channel_update_description">Shows update progress</string>
|
||||
|
||||
<string name="channel_transfer">Transfer</string>
|
||||
<string name="channel_transfer_description">Shows progress of transferring data to another device</string>
|
||||
|
||||
<string name="unable_to_connect">Unable to connect to hitomi.la</string>
|
||||
|
||||
<string name="lock_corrupted">Lock file corrupted! Please re-install Pupil</string>
|
||||
|
||||
Reference in New Issue
Block a user