wip
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user