Android Activity refactoring

This commit is contained in:
albexk 2023-11-23 20:30:03 +03:00
parent ccdcfdce8a
commit 0ba5d754d5
6 changed files with 339 additions and 18 deletions

View file

@ -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")

View file

@ -45,6 +45,7 @@ import java.io.IOException
import java.lang.Exception
import android.net.VpnService as BaseVpnService
const val VPN_CONFIG = "VPN_CONFIG"
class AmneziaVpnService : BaseVpnService()/* , LocalDnsService.Interface */ {

View file

@ -1,5 +0,0 @@
package org.amnezia.vpn
const val IMPORT_COMMAND_CODE = 1
const val IMPORT_ACTION_CODE = "import_action"
const val IMPORT_CONFIG_KEY = "CONFIG_DATA_KEY"

View file

@ -0,0 +1,45 @@
package org.amnezia.vpn
import android.os.Bundle
import android.os.Message
import kotlin.enums.enumEntries
sealed interface IpcMessage {
companion object {
@OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> extractFromMessage(msg: Message): T
where T : Enum<T>,
T : IpcMessage {
val values = enumEntries<T>()
if (msg.what !in values.indices) {
throw IllegalArgumentException("IPC action or event not found for the message: $msg")
}
return values[msg.what]
}
}
}
enum class ServiceEvent : IpcMessage {
CONNECTED,
DISCONNECTED,
STATUS,
STATISTICS_UPDATE,
ERROR
}
enum class Action : IpcMessage {
REGISTER_CLIENT,
CONNECT,
DISCONNECT,
REQUEST_STATUS,
REQUEST_STATISTICS
}
fun <T> T.packToMessage(): Message
where T : Enum<T>, T : IpcMessage = Message.obtain().also { it.what = ordinal }
fun <T> T.packToMessage(block: Bundle.() -> Unit): Message
where T : Enum<T>, T : IpcMessage = packToMessage().also { it.data = Bundle().apply(block) }
inline fun <reified T> Message.extractIpcMessage(): T
where T : Enum<T>, T : IpcMessage = IpcMessage.extractFromMessage<T>(this)

View file

@ -17,9 +17,9 @@ AndroidController::AndroidController() : QObject()
connect(this, &AndroidController::status, this,
[this](bool isVpnConnected) {
qDebug() << "Android event: status; connected:" << isVpnConnected;
if (isWaitingInitStatus) {
if (isWaitingStatus) {
qDebug() << "Android VPN service is alive, initialization by service status";
isWaitingInitStatus = false;
isWaitingStatus = false;
emit serviceIsAlive(isVpnConnected);
}
},
@ -29,6 +29,7 @@ AndroidController::AndroidController() : QObject()
this, &AndroidController::serviceDisconnected, this,
[this]() {
qDebug() << "Android event: service disconnected";
isWaitingStatus = true;
emit connectionStateChanged(Vpn::ConnectionState::Unknown);
},
Qt::QueuedConnection);
@ -140,7 +141,7 @@ void AndroidController::callActivityMethod(const char *methodName, const char *s
ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
{
isWaitingInitStatus = false;
isWaitingStatus = false;
auto config = QJsonDocument(vpnConfig).toJson();
callActivityMethod("start", "(Ljava/lang/String;)V",
QJniObject::fromString(config).object<jstring>());

View file

@ -37,7 +37,7 @@ signals:
void serviceIsAlive(bool connected);
private:
bool isWaitingInitStatus = true;
bool isWaitingStatus = true;
void qtAndroidControllerInitialized();