Android Activity refactoring
This commit is contained in:
parent
ccdcfdce8a
commit
0ba5d754d5
6 changed files with 339 additions and 18 deletions
|
|
@ -1,29 +1,304 @@
|
|||
package org.amnezia.vpn
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.os.DeadObjectException
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.os.RemoteException
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
import org.qtproject.qt.android.bindings.QtActivity
|
||||
|
||||
private const val TAG = "AmneziaActivity"
|
||||
|
||||
private const val CREATE_FILE_ACTION_CODE = 102
|
||||
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
|
||||
private const val CREATE_FILE_ACTION_CODE = 2
|
||||
|
||||
class AmneziaActivity : QtActivity() {
|
||||
|
||||
private lateinit var mainScope: CoroutineScope
|
||||
private val qtInitialized = CompletableDeferred<Unit>()
|
||||
private var isWaitingStatus = true
|
||||
private var isServiceConnected = false
|
||||
private var isInBoundState = false
|
||||
private var vpnServiceMessenger: Messenger? = null
|
||||
private var tmpFileContentToSave: String = ""
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == CREATE_FILE_ACTION_CODE && resultCode == RESULT_OK) {
|
||||
data?.data?.also { uri ->
|
||||
alterDocument(uri)
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
val event = msg.extractIpcMessage<ServiceEvent>()
|
||||
Log.d(TAG, "Handle event: $event")
|
||||
when (event) {
|
||||
ServiceEvent.CONNECTED -> {
|
||||
QtAndroidController.onVpnConnected()
|
||||
}
|
||||
|
||||
ServiceEvent.DISCONNECTED -> {
|
||||
QtAndroidController.onVpnDisconnected()
|
||||
}
|
||||
|
||||
ServiceEvent.STATUS -> {
|
||||
if (isWaitingStatus) {
|
||||
isWaitingStatus = false
|
||||
msg.data?.getStatus()?.let { (isConnected) ->
|
||||
QtAndroidController.onStatus(isConnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ServiceEvent.STATISTICS_UPDATE -> {
|
||||
msg.data?.getStatistics()?.let { (rxBytes, txBytes) ->
|
||||
QtAndroidController.onStatisticsUpdate(rxBytes, txBytes)
|
||||
}
|
||||
}
|
||||
|
||||
ServiceEvent.ERROR -> {
|
||||
// todo: add error reporting to Qt
|
||||
QtAndroidController.onServiceError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private val activityMessenger: Messenger by lazy(NONE) {
|
||||
Messenger(vpnServiceEventHandler)
|
||||
}
|
||||
|
||||
private val serviceConnection: ServiceConnection by lazy(NONE) {
|
||||
object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
Log.d(TAG, "Service ${name?.flattenToString()} was connected")
|
||||
// get a messenger from the service to send actions to the service
|
||||
vpnServiceMessenger = Messenger(service)
|
||||
// send a messenger to the service to process service events
|
||||
sendToService {
|
||||
Action.REGISTER_CLIENT.packToMessage().apply {
|
||||
replyTo = activityMessenger
|
||||
}
|
||||
}
|
||||
isServiceConnected = true
|
||||
if (isWaitingStatus) {
|
||||
sendToService(Action.REQUEST_STATUS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
|
||||
isServiceConnected = false
|
||||
vpnServiceMessenger = null
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
}
|
||||
|
||||
override fun onBindingDied(name: ComponentName?) {
|
||||
Log.w(TAG, "Binding to the ${name?.flattenToString()} unexpectedly died")
|
||||
doUnbindService()
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CheckVpnPermissionCallbacks(val onSuccess: () -> Unit, val onFail: () -> Unit)
|
||||
|
||||
private var checkVpnPermissionCallbacks: CheckVpnPermissionCallbacks? = null
|
||||
|
||||
/**
|
||||
* Activity overloaded methods
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.v(TAG, "Create Amnezia activity")
|
||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Log.v(TAG, "Start Amnezia activity")
|
||||
mainScope.launch {
|
||||
qtInitialized.await()
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.v(TAG, "Stop Amnezia activity")
|
||||
doUnbindService()
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.v(TAG, "Destroy Amnezia activity")
|
||||
mainScope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
CREATE_FILE_ACTION_CODE -> {
|
||||
when (resultCode) {
|
||||
RESULT_OK -> {
|
||||
data?.data?.let { uri ->
|
||||
alterDocument(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_VPN_PERMISSION_ACTION_CODE -> {
|
||||
when (resultCode) {
|
||||
RESULT_OK -> {
|
||||
Log.v(TAG, "Vpn permission granted")
|
||||
Toast.makeText(this, "Vpn permission granted", Toast.LENGTH_LONG).show()
|
||||
checkVpnPermissionCallbacks?.run { onSuccess() }
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Vpn permission denied, resultCode: $resultCode")
|
||||
Toast.makeText(this, "Vpn permission denied", Toast.LENGTH_LONG).show()
|
||||
checkVpnPermissionCallbacks?.run { onFail() }
|
||||
}
|
||||
}
|
||||
checkVpnPermissionCallbacks = null
|
||||
}
|
||||
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods of communication with the service
|
||||
*/
|
||||
private fun sendToService(messenger: Messenger, msg: Message) {
|
||||
try {
|
||||
messenger.send(msg)
|
||||
} catch (e: DeadObjectException) {
|
||||
Log.w(TAG, "Service messenger is dead")
|
||||
doUnbindService()
|
||||
} catch (e: RemoteException) {
|
||||
Log.w(TAG, "Sending a message to the service failed: ${e.message}")
|
||||
doUnbindService()
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun sendToService(msg: () -> Message) {
|
||||
vpnServiceMessenger?.let {
|
||||
sendToService(it, msg())
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun sendToService(msg: Action) {
|
||||
vpnServiceMessenger?.let {
|
||||
sendToService(it, msg.packToMessage())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods for service binding
|
||||
*/
|
||||
@MainThread
|
||||
private fun doBindService() {
|
||||
Log.v(TAG, "Bind service")
|
||||
Intent(this, AmneziaVpnService::class.java).also {
|
||||
bindService(it, serviceConnection, BIND_ABOVE_CLIENT)
|
||||
}
|
||||
isInBoundState = true
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun doUnbindService() {
|
||||
if (isInBoundState) {
|
||||
Log.v(TAG, "Unbind service")
|
||||
isWaitingStatus = true
|
||||
QtAndroidController.onServiceDisconnected()
|
||||
vpnServiceMessenger = null
|
||||
isServiceConnected = false
|
||||
isInBoundState = false
|
||||
unbindService(serviceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods of starting and stopping VpnService
|
||||
*/
|
||||
private fun checkVpnPermissionAndStart(vpnConfig: String) {
|
||||
checkVpnPermission(
|
||||
onSuccess = { startVpn(vpnConfig) },
|
||||
onFail = QtAndroidController::onVpnPermissionRejected
|
||||
)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun checkVpnPermission(onSuccess: () -> Unit, onFail: () -> Unit) {
|
||||
Log.v(TAG, "Check VPN permission")
|
||||
VpnService.prepare(applicationContext)?.let {
|
||||
checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail)
|
||||
startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE)
|
||||
return
|
||||
}
|
||||
onSuccess()
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private fun startVpn(vpnConfig: String) {
|
||||
if (isServiceConnected) {
|
||||
connectToVpn(vpnConfig)
|
||||
} else {
|
||||
isWaitingStatus = false
|
||||
startVpnService(vpnConfig)
|
||||
doBindService()
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToVpn(vpnConfig: String) {
|
||||
Log.v(TAG, "Connect to VPN")
|
||||
sendToService {
|
||||
Action.CONNECT.packToMessage {
|
||||
putString(VPN_CONFIG, vpnConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startVpnService(vpnConfig: String) {
|
||||
Log.v(TAG, "Start VPN service")
|
||||
Intent(this, AmneziaVpnService::class.java).apply {
|
||||
putExtra(VPN_CONFIG, vpnConfig)
|
||||
}.also {
|
||||
ContextCompat.startForegroundService(this, it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun disconnectFromVpn() {
|
||||
Log.v(TAG, "Disconnect from VPN")
|
||||
sendToService(Action.DISCONNECT)
|
||||
}
|
||||
|
||||
// saving file
|
||||
private fun alterDocument(uri: Uri) {
|
||||
try {
|
||||
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
|
||||
|
|
@ -46,19 +321,23 @@ class AmneziaActivity : QtActivity() {
|
|||
@Suppress("unused")
|
||||
fun qtAndroidControllerInitialized() {
|
||||
Log.v(TAG, "Qt Android controller initialized")
|
||||
Log.w(TAG, "Not yet implemented")
|
||||
qtInitialized.complete(Unit)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun start(vpnConfig: String) {
|
||||
Log.v(TAG, "Start VPN")
|
||||
Log.w(TAG, "Not yet implemented")
|
||||
mainScope.launch {
|
||||
checkVpnPermissionAndStart(vpnConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun stop() {
|
||||
Log.v(TAG, "Stop VPN")
|
||||
Log.w(TAG, "Not yet implemented")
|
||||
mainScope.launch {
|
||||
disconnectFromVpn()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue