From b0e194898e262836752cf78440653439d17f21d0 Mon Sep 17 00:00:00 2001
From: tom5079 <7948651+tom5079@users.noreply.github.com>
Date: Mon, 15 Apr 2024 18:19:49 -0700
Subject: [PATCH] wip
---
app/build.gradle | 6 +-
app/src/main/AndroidManifest.xml | 14 +-
app/src/main/java/xyz/quaver/pupil/Pupil.kt | 7 +
.../pupil/services/TransferClientService.kt | 97 +++++++++
.../pupil/services/TransferServerService.kt | 102 +++++++++
.../xyz/quaver/pupil/ui/TransferActivity.kt | 114 +++++++---
.../ui/fragment/TransferConnectedFragment.kt | 10 +
.../ui/fragment/TransferSelectDataFragment.kt | 37 ++++
app/src/main/res/drawable/check.xml | 13 ++
app/src/main/res/drawable/download.xml | 13 ++
app/src/main/res/drawable/heart.xml | 13 ++
app/src/main/res/drawable/history_rounded.xml | 9 +
.../layout/transfer_connected_fragment.xml | 28 +++
.../layout/transfer_select_data_fragment.xml | 196 ++++++++++++++++++
app/src/main/res/values-ja/strings.xml | 2 +
app/src/main/res/values-ko/strings.xml | 2 +
app/src/main/res/values/strings.xml | 3 +
17 files changed, 632 insertions(+), 34 deletions(-)
create mode 100644 app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt
create mode 100644 app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt
create mode 100644 app/src/main/res/drawable/check.xml
create mode 100644 app/src/main/res/drawable/download.xml
create mode 100644 app/src/main/res/drawable/heart.xml
create mode 100644 app/src/main/res/drawable/history_rounded.xml
create mode 100644 app/src/main/res/layout/transfer_connected_fragment.xml
create mode 100644 app/src/main/res/layout/transfer_select_data_fragment.xml
diff --git a/app/build.gradle b/app/build.gradle
index bceb8214..21128bba 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -82,13 +82,14 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2"
implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.3.2"
-
+
+ implementation "androidx.core:core-ktx:1.12.0"
implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
- implementation "androidx.constraintlayout:constraintlayout:2.1.3"
+ implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.gridlayout:gridlayout:1.0.0"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.work:work-runtime-ktx:2.7.1"
@@ -116,6 +117,7 @@ dependencies {
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
+ implementation "io.ktor:ktor-network:2.3.10"
implementation "com.tbuonomo.andrui:viewpagerdotsindicator:4.1.2"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 640c0d6d..0f98c2f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,10 +8,10 @@
-
-
+
+
@@ -54,7 +54,15 @@
+ android:foregroundServiceType="dataSync" />
+
+
+
+
("dark_mode")) {
diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt
new file mode 100644
index 00000000..a6346674
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/services/TransferClientService.kt
@@ -0,0 +1,97 @@
+package xyz.quaver.pupil.services
+
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import io.ktor.network.selector.SelectorManager
+import io.ktor.network.sockets.aSocket
+import io.ktor.network.sockets.openReadChannel
+import io.ktor.network.sockets.openWriteChannel
+import io.ktor.utils.io.writeStringUtf8
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.launch
+import xyz.quaver.pupil.R
+
+class TransferClientService : Service() {
+ private val selectorManager = SelectorManager(Dispatchers.IO)
+ private val channel = Channel()
+ private var job: Job? = null
+
+ private fun startForeground() = runCatching {
+ val notification = NotificationCompat.Builder(this, "transfer")
+ .setContentTitle("Pupil")
+ .setContentText("Transfer server is running")
+ .setSmallIcon(R.drawable.ic_notification)
+ .build()
+
+ ServiceCompat.startForeground(
+ this,
+ 1,
+ notification,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ } else 0
+ )
+ }
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val address = intent?.getStringExtra("address") ?: run {
+ stopSelf(startId)
+ return START_STICKY
+ }
+
+ startForeground()
+
+ Log.d("PUPILD", "Starting service with address $address")
+
+ job?.cancel()
+ job = CoroutineScope(Dispatchers.IO).launch {
+ Log.d("PUPILD", "Connecting to $address")
+
+ val socket = aSocket(selectorManager).tcp().connect(address, 12221)
+
+ Log.d("PUPILD", "Connected to $address")
+
+ val readChannel = socket.openReadChannel()
+ val writeChannel = socket.openWriteChannel(autoFlush = true)
+
+ runCatching {
+ while (true) {
+ val message = channel.receive()
+ Log.d("PUPILD", "Sending message $message!")
+ writeChannel.writeStringUtf8(message)
+ Log.d("PUPILD", readChannel.readUTF8Line(4).toString())
+ }
+ }.onFailure {
+ socket.close()
+ stopSelf(startId)
+ }
+ }
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ job?.cancel()
+ }
+
+ inner class Binder: android.os.Binder() {
+ fun sendMessage(message: String) {
+ Log.d("PUPILD", "Sending message $message")
+ channel.trySendBlocking(message + '\n')
+ }
+ }
+
+ private val binder = Binder()
+
+ override fun onBind(intent: Intent?): IBinder = binder
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt b/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt
new file mode 100644
index 00000000..1973269b
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/services/TransferServerService.kt
@@ -0,0 +1,102 @@
+package xyz.quaver.pupil.services
+
+import android.app.Service
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import io.ktor.network.selector.SelectorManager
+import io.ktor.network.sockets.ServerSocket
+import io.ktor.network.sockets.Socket
+import io.ktor.network.sockets.aSocket
+import io.ktor.network.sockets.openReadChannel
+import io.ktor.network.sockets.openWriteChannel
+import io.ktor.utils.io.writeStringUtf8
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import xyz.quaver.pupil.R
+
+class TransferServerService : Service() {
+ private val selectorManager = SelectorManager(Dispatchers.IO)
+ private var serverSocket: ServerSocket? = null
+ private val job = Job()
+
+ private fun startForeground() = runCatching {
+ val notification = NotificationCompat.Builder(this, "transfer")
+ .setContentTitle("Pupil")
+ .setContentText("Transfer server is running")
+ .setSmallIcon(R.drawable.ic_notification)
+ .build()
+
+ ServiceCompat.startForeground(
+ this,
+ 1,
+ notification,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ } else 0
+ )
+ }
+
+ private suspend fun handleConnection(socket: Socket) {
+ val readChannel = socket.openReadChannel()
+ val writeChannel = socket.openWriteChannel(autoFlush = true)
+
+ runCatching {
+ while (true) {
+ if (readChannel.readUTF8Line(8) == "ping") {
+ writeChannel.writeStringUtf8("pong\n")
+ }
+ }
+ }.onFailure {
+ socket.close()
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val address = intent?.getStringExtra("address") ?: run {
+ stopSelf(startId)
+ return START_STICKY
+ }
+
+ if (serverSocket != null) {
+ return START_STICKY
+ }
+
+ startForeground()
+
+ val serverSocket = aSocket(selectorManager).tcp().bind(address, 12221).also {
+ this@TransferServerService.serverSocket = it
+ }
+
+ CoroutineScope(Dispatchers.IO + job).launch {
+ while (true) {
+ Log.d("PUPILD", "Waiting for connection")
+ val socket = serverSocket.accept()
+ Log.d("PUPILD", "Accepted connection from ${socket.remoteAddress}")
+ launch { handleConnection(socket) }
+ }
+ }
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ job.cancel()
+ serverSocket?.close()
+ }
+
+ inner class Binder: android.os.Binder() {
+ fun getService() = this@TransferServerService
+ }
+
+ private val binder = Binder()
+
+ override fun onBind(intent: Intent?): IBinder = binder
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt
index 50d61087..95b3383c 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/TransferActivity.kt
@@ -3,7 +3,10 @@ package xyz.quaver.pupil.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Intent
import android.content.IntentFilter
+import android.content.ServiceConnection
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.net.wifi.WpsInfo
@@ -15,11 +18,13 @@ import android.net.wifi.p2p.WifiP2pManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
+import android.os.IBinder
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LiveData
@@ -27,13 +32,23 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.cancellable
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import xyz.quaver.pupil.R
import xyz.quaver.pupil.receiver.WifiDirectBroadcastReceiver
+import xyz.quaver.pupil.services.TransferClientService
+import xyz.quaver.pupil.services.TransferServerService
+import xyz.quaver.pupil.ui.fragment.TransferConnectedFragment
import xyz.quaver.pupil.ui.fragment.TransferDirectionFragment
import xyz.quaver.pupil.ui.fragment.TransferPermissionFragment
+import xyz.quaver.pupil.ui.fragment.TransferSelectDataFragment
import xyz.quaver.pupil.ui.fragment.TransferTargetFragment
import xyz.quaver.pupil.ui.fragment.TransferWaitForConnectionFragment
import kotlin.coroutines.resume
@@ -66,6 +81,18 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
}
}
+ private var serviceBinder: TransferClientService.Binder? = null
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ serviceBinder = service as TransferClientService.Binder
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ serviceBinder = null
+ }
+ }
+
private fun checkPermission(force: Boolean = false): Boolean {
val permissionRequired = if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) {
Manifest.permission.ACCESS_FINE_LOCATION
@@ -110,40 +137,25 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
}
manager.connect(channel, config, object: WifiP2pManager.ActionListener {
- override fun onSuccess() {
- Log.d("PUPILD", "Connection successful")
- }
+ override fun onSuccess() { }
override fun onFailure(reason: Int) {
- Log.d("PUPILD", "Connection failed: $reason")
- viewModel.setPeers(null)
+ viewModel.connect(null)
}
})
}
-
- viewModel.connectionInfo.observe(this) { info ->
- if (info == null) { return@observe }
-
- if (info.groupFormed && info.isGroupOwner) {
- // Do something
- Log.d("PUPILD", "Group formed and is group owner")
- Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}")
- } else if (info.groupFormed) {
- // Do something
- Log.d("PUPILD", "Group formed")
- Log.d("PUPILD", "Group owner IP: ${info.groupOwnerAddress.hostAddress}")
- Log.d("PUPILD", "Local IP: ${info.groupOwnerAddress.hostAddress}")
- Log.d("PUPILD", "Is group owner: ${info.isGroupOwner}")
- }
- }
-
lifecycleScope.launch {
- viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { step ->
+ viewModel.messageQueue.consumeEach {
+ serviceBinder?.sendMessage(it)
+ }
+
+ viewModel.step.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).cancellable().collect step@{ step ->
+ Log.d("PUPILD", "Step: $step")
when (step) {
TransferStep.TARGET,
TransferStep.TARGET_FORCE -> {
if (!checkPermission(step == TransferStep.TARGET_FORCE)) {
- return@collect
+ return@step
}
manager.discoverPeers(channel, object: WifiP2pManager.ActionListener {
@@ -156,6 +168,17 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
supportFragmentManager.commit {
replace(R.id.fragment_container_view, TransferTargetFragment())
}
+
+ val hostAddress = viewModel.connectionInfo.filterNotNull().first {
+ it.groupFormed
+ }.groupOwnerAddress.hostAddress
+
+ val intent = Intent(this@TransferActivity, TransferClientService::class.java).also {
+ it.putExtra("address", hostAddress)
+ }
+ bindService(intent, serviceConnection, BIND_AUTO_CREATE)
+
+ viewModel.setStep(TransferStep.SELECT_DATA)
}
TransferStep.DIRECTION -> {
supportFragmentManager.commit {
@@ -168,7 +191,8 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
}
}
TransferStep.WAIT_FOR_CONNECTION -> {
- if (!checkPermission()) { return@collect }
+ Log.d("PUPILD", "wait for connection")
+ if (!checkPermission()) { return@step }
runCatching {
suspendCoroutine { continuation ->
@@ -206,6 +230,21 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
}
})
}
+
+ supportFragmentManager.commit {
+ replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
+ }
+
+ val address = viewModel.connectionInfo.filterNotNull().first {
+ it.groupFormed && it.isGroupOwner
+ }.groupOwnerAddress.hostAddress
+
+ val intent = Intent(this@TransferActivity, TransferServerService::class.java).also {
+ it.putExtra("address", address)
+ }
+ ContextCompat.startForegroundService(this@TransferActivity, intent)
+
+ viewModel.setStep(TransferStep.CONNECTED)
}.onFailure {
Log.e("PUPILD", "Failed to create group", it)
}
@@ -214,6 +253,16 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
replace(R.id.fragment_container_view, TransferWaitForConnectionFragment())
}
}
+ TransferStep.CONNECTED -> {
+ supportFragmentManager.commit {
+ replace(R.id.fragment_container_view, TransferConnectedFragment())
+ }
+ }
+ TransferStep.SELECT_DATA -> {
+ supportFragmentManager.commit {
+ replace(R.id.fragment_container_view, TransferSelectDataFragment())
+ }
+ }
}
}
}
@@ -236,7 +285,7 @@ class TransferActivity : AppCompatActivity(R.layout.transfer_activity) {
}
enum class TransferStep {
- TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION
+ TARGET, TARGET_FORCE, DIRECTION, PERMISSION, WAIT_FOR_CONNECTION, CONNECTED, SELECT_DATA
}
enum class ErrorType {
@@ -258,13 +307,16 @@ class TransferViewModel : ViewModel() {
private val _peers: MutableLiveData = MutableLiveData(null)
val peers: LiveData = _peers
- private val _connectionInfo: MutableLiveData = MutableLiveData(null)
- val connectionInfo: LiveData = _connectionInfo
+ private val _connectionInfo: MutableStateFlow = MutableStateFlow(null)
+ val connectionInfo: StateFlow = _connectionInfo
private val _peerToConnect: MutableLiveData = MutableLiveData(null)
val peerToConnect: LiveData = _peerToConnect
+ val messageQueue: Channel = Channel()
+
fun setStep(step: TransferStep) {
+ Log.d("PUPILD", "Set step: $step")
_step.value = step
}
@@ -288,7 +340,11 @@ class TransferViewModel : ViewModel() {
_error.value = error
}
- fun connect(device: WifiP2pDevice) {
+ fun connect(device: WifiP2pDevice?) {
_peerToConnect.value = device
}
+
+ fun ping() {
+ messageQueue.trySend("ping")
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt
new file mode 100644
index 00000000..a434d078
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferConnectedFragment.kt
@@ -0,0 +1,10 @@
+package xyz.quaver.pupil.ui.fragment
+
+import androidx.fragment.app.Fragment
+import xyz.quaver.pupil.R
+
+class TransferConnectedFragment: Fragment(R.layout.transfer_connected_fragment) {
+
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt
new file mode 100644
index 00000000..c71f3f4b
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/ui/fragment/TransferSelectDataFragment.kt
@@ -0,0 +1,37 @@
+package xyz.quaver.pupil.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import xyz.quaver.pupil.databinding.TransferSelectDataFragmentBinding
+import xyz.quaver.pupil.ui.TransferViewModel
+
+class TransferSelectDataFragment: Fragment() {
+
+ private var _binding: TransferSelectDataFragmentBinding? = null
+ private val binding get() = _binding!!
+
+ private val viewModel: TransferViewModel by activityViewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ _binding = TransferSelectDataFragmentBinding.inflate(inflater, container, false)
+
+ binding.checkAll.setOnCheckedChangeListener { _, isChecked ->
+ viewModel.ping()
+ }
+
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml
new file mode 100644
index 00000000..d075199a
--- /dev/null
+++ b/app/src/main/res/drawable/check.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml
new file mode 100644
index 00000000..3f16b5b8
--- /dev/null
+++ b/app/src/main/res/drawable/download.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/heart.xml b/app/src/main/res/drawable/heart.xml
new file mode 100644
index 00000000..07d57bd0
--- /dev/null
+++ b/app/src/main/res/drawable/heart.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/app/src/main/res/drawable/history_rounded.xml b/app/src/main/res/drawable/history_rounded.xml
new file mode 100644
index 00000000..b47b45d1
--- /dev/null
+++ b/app/src/main/res/drawable/history_rounded.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/transfer_connected_fragment.xml b/app/src/main/res/layout/transfer_connected_fragment.xml
new file mode 100644
index 00000000..12d1ffee
--- /dev/null
+++ b/app/src/main/res/layout/transfer_connected_fragment.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/transfer_select_data_fragment.xml b/app/src/main/res/layout/transfer_select_data_fragment.xml
new file mode 100644
index 00000000..a750e826
--- /dev/null
+++ b/app/src/main/res/layout/transfer_select_data_fragment.xml
@@ -0,0 +1,196 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 1b320e63..3a318552 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -48,6 +48,7 @@
ダウンロード
ページ移動
現ページ番号: %1$d\nページ数: %2$d
+ 転送
hitomi.laに接続できません
%1$dページへ移動
ダウンロード削除
@@ -160,4 +161,5 @@
ネットワーク
ダウンロードデータベースを再構築
他の機器にデータを転送
+ 他の機器へのデータ転送の進捗度を表示
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 583f3f15..4edcfafc 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -47,6 +47,7 @@
다운로드
페이지 이동
현재 페이지: %1$d\n페이지 수: %2$d
+ 전송
hitomi.la에 연결할 수 없습니다
%1$d 페이지로 이동
다운로드 삭제
@@ -160,4 +161,5 @@
네트워크
다운로드 데이터베이스 복구
다른 기기에 데이터 전송
+ 다른 기기에 데이터 전송 시 상태 표시
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3a60e8c4..87164ae8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -43,6 +43,9 @@
Update
Shows update progress
+ Transfer
+ Shows progress of transferring data to another device
+
Unable to connect to hitomi.la
Lock file corrupted! Please re-install Pupil