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
|
package org.amnezia.vpn
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
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.FileNotFoundException
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
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
|
import org.qtproject.qt.android.bindings.QtActivity
|
||||||
|
|
||||||
private const val TAG = "AmneziaActivity"
|
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() {
|
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 = ""
|
private var tmpFileContentToSave: String = ""
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||||
if (requestCode == CREATE_FILE_ACTION_CODE && resultCode == RESULT_OK) {
|
object : Handler(Looper.getMainLooper()) {
|
||||||
data?.data?.also { uri ->
|
override fun handleMessage(msg: Message) {
|
||||||
alterDocument(uri)
|
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) {
|
private fun alterDocument(uri: Uri) {
|
||||||
try {
|
try {
|
||||||
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
|
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
|
||||||
|
@ -46,19 +321,23 @@ class AmneziaActivity : QtActivity() {
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun qtAndroidControllerInitialized() {
|
fun qtAndroidControllerInitialized() {
|
||||||
Log.v(TAG, "Qt Android controller initialized")
|
Log.v(TAG, "Qt Android controller initialized")
|
||||||
Log.w(TAG, "Not yet implemented")
|
qtInitialized.complete(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun start(vpnConfig: String) {
|
fun start(vpnConfig: String) {
|
||||||
Log.v(TAG, "Start VPN")
|
Log.v(TAG, "Start VPN")
|
||||||
Log.w(TAG, "Not yet implemented")
|
mainScope.launch {
|
||||||
|
checkVpnPermissionAndStart(vpnConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun stop() {
|
fun stop() {
|
||||||
Log.v(TAG, "Stop VPN")
|
Log.v(TAG, "Stop VPN")
|
||||||
Log.w(TAG, "Not yet implemented")
|
mainScope.launch {
|
||||||
|
disconnectFromVpn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
|
|
|
@ -45,6 +45,7 @@ import java.io.IOException
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import android.net.VpnService as BaseVpnService
|
import android.net.VpnService as BaseVpnService
|
||||||
|
|
||||||
|
const val VPN_CONFIG = "VPN_CONFIG"
|
||||||
|
|
||||||
class AmneziaVpnService : BaseVpnService()/* , LocalDnsService.Interface */ {
|
class AmneziaVpnService : BaseVpnService()/* , LocalDnsService.Interface */ {
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
45
client/android/src/org/amnezia/vpn/IpcMessage.kt
Normal file
45
client/android/src/org/amnezia/vpn/IpcMessage.kt
Normal 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)
|
|
@ -17,9 +17,9 @@ AndroidController::AndroidController() : QObject()
|
||||||
connect(this, &AndroidController::status, this,
|
connect(this, &AndroidController::status, this,
|
||||||
[this](bool isVpnConnected) {
|
[this](bool isVpnConnected) {
|
||||||
qDebug() << "Android event: status; connected:" << isVpnConnected;
|
qDebug() << "Android event: status; connected:" << isVpnConnected;
|
||||||
if (isWaitingInitStatus) {
|
if (isWaitingStatus) {
|
||||||
qDebug() << "Android VPN service is alive, initialization by service status";
|
qDebug() << "Android VPN service is alive, initialization by service status";
|
||||||
isWaitingInitStatus = false;
|
isWaitingStatus = false;
|
||||||
emit serviceIsAlive(isVpnConnected);
|
emit serviceIsAlive(isVpnConnected);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -29,6 +29,7 @@ AndroidController::AndroidController() : QObject()
|
||||||
this, &AndroidController::serviceDisconnected, this,
|
this, &AndroidController::serviceDisconnected, this,
|
||||||
[this]() {
|
[this]() {
|
||||||
qDebug() << "Android event: service disconnected";
|
qDebug() << "Android event: service disconnected";
|
||||||
|
isWaitingStatus = true;
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Unknown);
|
emit connectionStateChanged(Vpn::ConnectionState::Unknown);
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
@ -140,7 +141,7 @@ void AndroidController::callActivityMethod(const char *methodName, const char *s
|
||||||
|
|
||||||
ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
|
ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
|
||||||
{
|
{
|
||||||
isWaitingInitStatus = false;
|
isWaitingStatus = false;
|
||||||
auto config = QJsonDocument(vpnConfig).toJson();
|
auto config = QJsonDocument(vpnConfig).toJson();
|
||||||
callActivityMethod("start", "(Ljava/lang/String;)V",
|
callActivityMethod("start", "(Ljava/lang/String;)V",
|
||||||
QJniObject::fromString(config).object<jstring>());
|
QJniObject::fromString(config).object<jstring>());
|
||||||
|
|
|
@ -37,7 +37,7 @@ signals:
|
||||||
void serviceIsAlive(bool connected);
|
void serviceIsAlive(bool connected);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isWaitingInitStatus = true;
|
bool isWaitingStatus = true;
|
||||||
|
|
||||||
void qtAndroidControllerInitialized();
|
void qtAndroidControllerInitialized();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue