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")
|
||||
|
|
|
@ -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 */ {
|
||||
|
||||
|
|
|
@ -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,
|
||||
[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>());
|
||||
|
|
|
@ -37,7 +37,7 @@ signals:
|
|||
void serviceIsAlive(bool connected);
|
||||
|
||||
private:
|
||||
bool isWaitingInitStatus = true;
|
||||
bool isWaitingStatus = true;
|
||||
|
||||
void qtAndroidControllerInitialized();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue