Files
amnezia-client/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
T

516 lines
16 KiB
Kotlin
Raw Normal View History

2023-11-16 20:16:28 +03:00
package org.amnezia.vpn
import android.app.AlertDialog
2023-11-23 20:30:03 +03:00
import android.content.ComponentName
import android.content.Intent
2023-12-26 16:23:05 +03:00
import android.content.Intent.EXTRA_MIME_TYPES
2023-12-11 22:56:01 +03:00
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
2023-11-23 20:30:03 +03:00
import android.content.ServiceConnection
import android.content.pm.PackageManager
2024-04-01 18:45:00 +07:00
import android.graphics.Bitmap
import android.net.Uri
2023-11-23 20:30:03 +03:00
import android.net.VpnService
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.provider.Settings
2024-03-06 04:18:19 +03:00
import android.view.WindowManager.LayoutParams
2023-12-26 16:23:05 +03:00
import android.webkit.MimeTypeMap
2023-11-23 20:30:03 +03:00
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import java.io.IOException
2023-11-23 20:30:03 +03:00
import kotlin.LazyThreadSafetyMode.NONE
2023-12-26 16:23:05 +03:00
import kotlin.text.RegexOption.IGNORE_CASE
2024-04-01 18:45:00 +07:00
import AppListProvider
2023-11-23 20:30:03 +03:00
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
2024-04-01 18:45:00 +07:00
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
2023-11-23 20:30:03 +03:00
import org.amnezia.vpn.protocol.getStatistics
import org.amnezia.vpn.protocol.getStatus
import org.amnezia.vpn.qt.QtAndroidController
import org.amnezia.vpn.util.Log
2022-12-23 17:32:20 +03:00
import org.qtproject.qt.android.bindings.QtActivity
private const val TAG = "AmneziaActivity"
2024-03-04 18:08:55 +03:00
const val ACTIVITY_MESSENGER_NAME = "Activity"
2023-03-29 16:09:46 +03:00
2023-11-23 20:30:03 +03:00
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
2023-12-26 16:23:05 +03:00
private const val OPEN_FILE_ACTION_CODE = 3
class AmneziaActivity : QtActivity() {
2023-11-23 20:30:03 +03:00
private lateinit var mainScope: CoroutineScope
private val qtInitialized = CompletableDeferred<Unit>()
private var isWaitingStatus = true
private var isServiceConnected = false
private var isInBoundState = false
2023-11-24 17:10:08 +03:00
private lateinit var vpnServiceMessenger: IpcMessenger
private var tmpFileContentToSave: String = ""
2023-11-23 20:30:03 +03:00
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) {
2024-03-04 18:08:55 +03:00
ServiceEvent.STATUS_CHANGED -> {
msg.data?.getStatus()?.let { (state) ->
Log.d(TAG, "Handle protocol state: $state")
QtAndroidController.onVpnStateChanged(state.ordinal)
}
}
2023-11-23 20:30:03 +03:00
ServiceEvent.STATUS -> {
if (isWaitingStatus) {
isWaitingStatus = false
2024-03-04 18:08:55 +03:00
msg.data?.getStatus()?.let { QtAndroidController.onStatus(it) }
2023-11-23 20:30:03 +03:00
}
}
ServiceEvent.STATISTICS_UPDATE -> {
msg.data?.getStatistics()?.let { (rxBytes, txBytes) ->
QtAndroidController.onStatisticsUpdate(rxBytes, txBytes)
}
}
ServiceEvent.ERROR -> {
2024-03-04 18:08:55 +03:00
msg.data?.getString(MSG_ERROR)?.let { error ->
2023-11-24 21:51:09 +03:00
Log.e(TAG, "From VpnService: $error")
}
2023-11-23 20:30:03 +03:00
// todo: add error reporting to Qt
QtAndroidController.onServiceError()
}
}
}
}
}
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
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.set(Messenger(service))
2023-11-23 20:30:03 +03:00
// send a messenger to the service to process service events
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(
Action.REGISTER_CLIENT.packToMessage {
putString(MSG_CLIENT_NAME, ACTIVITY_MESSENGER_NAME)
},
replyTo = activityMessenger
)
2023-11-23 20:30:03 +03:00
isServiceConnected = true
if (isWaitingStatus) {
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(Action.REQUEST_STATUS, replyTo = activityMessenger)
2023-11-23 20:30:03 +03:00
}
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.w(TAG, "Service ${name?.flattenToString()} was unexpectedly disconnected")
isServiceConnected = false
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.reset()
2023-11-23 20:30:03 +03:00
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
2024-03-04 18:08:55 +03:00
doBindService()
2023-11-23 20:30:03 +03:00
}
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)
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Create Amnezia activity: $intent")
2023-11-23 20:30:03 +03:00
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
2023-11-24 17:10:08 +03:00
vpnServiceMessenger = IpcMessenger(
2024-03-04 18:08:55 +03:00
"VpnService",
onDeadObjectException = {
doUnbindService()
doBindService()
}
2023-11-24 17:10:08 +03:00
)
2023-12-11 22:56:01 +03:00
intent?.let(::processIntent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
2024-01-20 16:40:12 +03:00
Log.d(TAG, "onNewIntent: $intent")
2023-12-11 22:56:01 +03:00
intent?.let(::processIntent)
}
private fun processIntent(intent: Intent) {
// disable config import when starting activity from history
if (intent.flags and FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY == 0) {
if (intent.action == ACTION_IMPORT_CONFIG) {
intent.getStringExtra(EXTRA_CONFIG)?.let {
mainScope.launch {
qtInitialized.await()
QtAndroidController.onConfigImported(it)
}
}
}
}
2023-11-23 20:30:03 +03:00
}
override fun onStart() {
super.onStart()
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Start Amnezia activity")
2023-11-23 20:30:03 +03:00
mainScope.launch {
qtInitialized.await()
doBindService()
}
}
override fun onStop() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Stop Amnezia activity")
2023-11-23 20:30:03 +03:00
doUnbindService()
super.onStop()
}
override fun onDestroy() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Destroy Amnezia activity")
2023-11-23 20:30:03 +03:00
mainScope.cancel()
super.onDestroy()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
2023-11-23 20:30:03 +03:00
when (requestCode) {
CREATE_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
data?.data?.let { uri ->
alterDocument(uri)
}
}
}
}
2023-12-26 16:23:05 +03:00
OPEN_FILE_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> data?.data?.toString() ?: ""
else -> ""
}.let { uri ->
QtAndroidController.onFileOpened(uri)
}
}
2023-11-23 20:30:03 +03:00
CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Vpn permission granted")
Toast.makeText(this, resources.getText(R.string.vpnGranted), Toast.LENGTH_LONG).show()
2023-11-23 20:30:03 +03:00
checkVpnPermissionCallbacks?.run { onSuccess() }
}
else -> {
Log.w(TAG, "Vpn permission denied, resultCode: $resultCode")
showOnVpnPermissionRejectDialog()
2023-11-23 20:30:03 +03:00
checkVpnPermissionCallbacks?.run { onFail() }
}
}
checkVpnPermissionCallbacks = null
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
/**
* Methods for service binding
*/
@MainThread
private fun doBindService() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Bind service")
2023-11-23 20:30:03 +03:00
Intent(this, AmneziaVpnService::class.java).also {
2024-03-04 18:08:55 +03:00
bindService(it, serviceConnection, BIND_ABOVE_CLIENT and BIND_AUTO_CREATE)
2023-11-23 20:30:03 +03:00
}
isInBoundState = true
}
@MainThread
private fun doUnbindService() {
if (isInBoundState) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Unbind service")
2023-11-23 20:30:03 +03:00
isWaitingStatus = true
QtAndroidController.onServiceDisconnected()
isServiceConnected = false
2024-03-04 18:08:55 +03:00
vpnServiceMessenger.send(Action.UNREGISTER_CLIENT, activityMessenger)
vpnServiceMessenger.reset()
2023-11-23 20:30:03 +03:00
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) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Check VPN permission")
2023-11-23 20:30:03 +03:00
VpnService.prepare(applicationContext)?.let {
checkVpnPermissionCallbacks = CheckVpnPermissionCallbacks(onSuccess, onFail)
startActivityForResult(it, CHECK_VPN_PERMISSION_ACTION_CODE)
return
}
onSuccess()
}
private fun showOnVpnPermissionRejectDialog() {
AlertDialog.Builder(this)
.setTitle(R.string.vpnSetupFailed)
.setMessage(R.string.vpnSetupFailedMessage)
.setNegativeButton(R.string.ok) { _, _ -> }
.setPositiveButton(R.string.openVpnSettings) { _, _ ->
startActivity(Intent(Settings.ACTION_VPN_SETTINGS))
}
.show()
}
2023-11-23 20:30:03 +03:00
@MainThread
private fun startVpn(vpnConfig: String) {
if (isServiceConnected) {
connectToVpn(vpnConfig)
} else {
isWaitingStatus = false
startVpnService(vpnConfig)
doBindService()
}
}
private fun connectToVpn(vpnConfig: String) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Connect to VPN")
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.send {
2023-11-23 20:30:03 +03:00
Action.CONNECT.packToMessage {
2024-03-04 18:08:55 +03:00
putString(MSG_VPN_CONFIG, vpnConfig)
2023-03-29 16:09:46 +03:00
}
}
}
2023-11-23 20:30:03 +03:00
private fun startVpnService(vpnConfig: String) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Start VPN service")
2023-11-23 20:30:03 +03:00
Intent(this, AmneziaVpnService::class.java).apply {
2024-03-04 18:08:55 +03:00
putExtra(MSG_VPN_CONFIG, vpnConfig)
2023-11-23 20:30:03 +03:00
}.also {
ContextCompat.startForegroundService(this, it)
}
}
private fun disconnectFromVpn() {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Disconnect from VPN")
2023-11-24 17:10:08 +03:00
vpnServiceMessenger.send(Action.DISCONNECT)
2023-11-23 20:30:03 +03:00
}
// saving file
private fun alterDocument(uri: Uri) {
2022-12-23 17:32:20 +03:00
try {
contentResolver.openOutputStream(uri)?.use { os ->
os.bufferedWriter().use { it.write(tmpFileContentToSave) }
}
} catch (e: IOException) {
2022-12-23 17:32:20 +03:00
e.printStackTrace()
}
tmpFileContentToSave = ""
}
/**
* Methods called by Qt
*/
@Suppress("unused")
fun qtAndroidControllerInitialized() {
Log.v(TAG, "Qt Android controller initialized")
2023-11-23 20:30:03 +03:00
qtInitialized.complete(Unit)
}
@Suppress("unused")
fun start(vpnConfig: String) {
Log.v(TAG, "Start VPN")
2023-11-23 20:30:03 +03:00
mainScope.launch {
checkVpnPermissionAndStart(vpnConfig)
}
2022-12-23 17:32:20 +03:00
}
@Suppress("unused")
fun stop() {
Log.v(TAG, "Stop VPN")
2023-11-23 20:30:03 +03:00
mainScope.launch {
disconnectFromVpn()
}
2022-12-23 17:32:20 +03:00
}
2024-03-04 18:08:55 +03:00
@Suppress("unused")
fun resetLastServer(index: Int) {
Log.v(TAG, "Reset server: $index")
mainScope.launch {
VpnStateStore.store {
if (index == -1 || it.serverIndex == index) {
VpnState.defaultState
} else if (it.serverIndex > index) {
it.copy(serverIndex = it.serverIndex - 1)
} else {
it
}
}
}
}
@Suppress("unused")
fun saveFile(fileName: String, data: String) {
2024-01-20 16:40:12 +03:00
Log.d(TAG, "Save file $fileName")
mainScope.launch {
tmpFileContentToSave = data
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/*"
putExtra(Intent.EXTRA_TITLE, fileName)
}.also {
startActivityForResult(it, CREATE_FILE_ACTION_CODE)
}
2022-12-23 17:32:20 +03:00
}
}
2023-12-26 16:23:05 +03:00
@Suppress("unused")
fun openFile(filter: String?) {
Log.v(TAG, "Open file with filter: $filter")
val mimeTypes = if (!filter.isNullOrEmpty()) {
2024-04-05 14:04:04 +03:00
val extensionRegex = "\\*\\.([a-z0-9]+)".toRegex(IGNORE_CASE)
2023-12-26 16:23:05 +03:00
val mime = MimeTypeMap.getSingleton()
extensionRegex.findAll(filter).map {
2024-04-05 14:04:04 +03:00
it.groups[1]?.value?.let { mime.getMimeTypeFromExtension(it) } ?: "*/*"
}.toSet()
2023-12-26 16:23:05 +03:00
} else emptySet()
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
2024-01-20 16:40:12 +03:00
Log.v(TAG, "File mimyType filter: $mimeTypes")
2024-04-05 14:04:04 +03:00
if ("*/*" in mimeTypes) {
type = "*/*"
} else {
when (mimeTypes.size) {
1 -> type = mimeTypes.first()
in 2..Int.MAX_VALUE -> {
type = "*/*"
putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
2023-12-26 16:23:05 +03:00
2024-04-05 14:04:04 +03:00
else -> type = "*/*"
2023-12-26 16:23:05 +03:00
}
}
}.also {
startActivityForResult(it, OPEN_FILE_ACTION_CODE)
}
}
@Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text")
}
@Suppress("unused")
fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)
@Suppress("unused")
fun startQrCodeReader() {
Log.v(TAG, "Start camera")
2023-11-21 22:48:52 +03:00
Intent(this, CameraActivity::class.java).also {
startActivity(it)
}
}
2024-01-20 16:40:12 +03:00
@Suppress("unused")
fun setSaveLogs(enabled: Boolean) {
Log.d(TAG, "Set save logs: $enabled")
mainScope.launch {
Log.saveLogs = enabled
vpnServiceMessenger.send {
Action.SET_SAVE_LOGS.packToMessage {
2024-03-04 18:08:55 +03:00
putBoolean(MSG_SAVE_LOGS, enabled)
2024-01-20 16:40:12 +03:00
}
}
}
}
@Suppress("unused")
fun exportLogsFile(fileName: String) {
Log.v(TAG, "Export logs file")
saveFile(fileName, Log.getLogs())
}
@Suppress("unused")
fun clearLogs() {
Log.v(TAG, "Clear logs")
Log.clearLogs()
}
2024-03-06 04:18:19 +03:00
@Suppress("unused")
fun setScreenshotsEnabled(enabled: Boolean) {
Log.v(TAG, "Set screenshots enabled: $enabled")
mainScope.launch {
val flag = if (enabled) 0 else LayoutParams.FLAG_SECURE
window.setFlags(flag, LayoutParams.FLAG_SECURE)
}
}
@Suppress("unused")
fun minimizeApp() {
Log.v(TAG, "Minimize application")
mainScope.launch {
moveTaskToBack(false)
}
}
2024-04-01 18:45:00 +07:00
@Suppress("unused")
fun getAppList(): String {
Log.v(TAG, "Get app list")
var appList = ""
runBlocking {
mainScope.launch {
withContext(Dispatchers.IO) {
appList = AppListProvider.getAppList(packageManager, packageName)
}
}.join()
}
return appList
}
@Suppress("unused")
fun getAppIcon(packageName: String, width: Int, height: Int): Bitmap {
Log.v(TAG, "Get app icon: $packageName")
return AppListProvider.getAppIcon(packageManager, packageName, width, height)
}
}