mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
7351fe9633
Spelling fixed
332 lines
10 KiB
Kotlin
332 lines
10 KiB
Kotlin
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.amnezia.vpn.qt;
|
|
|
|
import android.Manifest
|
|
import android.content.ClipData
|
|
import android.content.ClipboardManager
|
|
import android.content.ComponentName
|
|
import android.content.ContentResolver
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.ServiceConnection
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.*
|
|
import android.provider.MediaStore
|
|
import android.util.Log
|
|
import android.view.KeyEvent
|
|
import android.widget.Toast
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.content.ContextCompat
|
|
import org.amnezia.vpn.VPNService
|
|
import org.amnezia.vpn.VPNServiceBinder
|
|
import org.amnezia.vpn.IMPORT_COMMAND_CODE
|
|
import org.amnezia.vpn.IMPORT_ACTION_CODE
|
|
import org.amnezia.vpn.IMPORT_CONFIG_KEY
|
|
import org.qtproject.qt.android.bindings.QtActivity
|
|
import java.io.*
|
|
|
|
class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
|
|
|
|
private var configString: String? = null
|
|
private var vpnServiceBinder: IBinder? = null
|
|
private var isBound = false
|
|
set(value) {
|
|
field = value
|
|
|
|
if (value && configString != null) {
|
|
sendImportConfigCommand()
|
|
}
|
|
}
|
|
|
|
private val TAG = "VPNActivity"
|
|
|
|
private val CAMERA_ACTION_CODE = 101
|
|
private val CREATE_FILE_ACTION_CODE = 102
|
|
|
|
private var tmpFileContentToSave: String = ""
|
|
|
|
private val delayedCommands: ArrayList<Pair<Int, String>> = ArrayList()
|
|
|
|
companion object {
|
|
private lateinit var instance: VPNActivity
|
|
|
|
@JvmStatic fun getInstance(): VPNActivity {
|
|
return instance
|
|
}
|
|
|
|
@JvmStatic fun connectService() {
|
|
VPNActivity.getInstance().initServiceConnection()
|
|
}
|
|
|
|
@JvmStatic fun startQrCodeReader() {
|
|
VPNActivity.getInstance().startQrCodeActivity()
|
|
}
|
|
|
|
@JvmStatic fun sendToService(actionCode: Int, body: String) {
|
|
VPNActivity.getInstance().dispatchParcel(actionCode, body)
|
|
}
|
|
|
|
@JvmStatic fun saveFileAs(fileContent: String, suggestedName: String) {
|
|
VPNActivity.getInstance().saveFile(fileContent, suggestedName)
|
|
}
|
|
|
|
@JvmStatic fun putTextToClipboard(text: String) {
|
|
VPNActivity.getInstance().putToClipboard(text)
|
|
}
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
instance = this
|
|
|
|
val newIntent = intent
|
|
val newIntentAction: String? = newIntent.action
|
|
|
|
if (newIntent != null && newIntentAction != null && newIntentAction == "org.amnezia.vpn.qt.IMPORT_CONFIG") {
|
|
configString = newIntent.getStringExtra("CONFIG")
|
|
}
|
|
}
|
|
|
|
private fun startQrCodeActivity() {
|
|
val intent = Intent(this, CameraActivity::class.java)
|
|
startActivityForResult(intent, CAMERA_ACTION_CODE)
|
|
}
|
|
|
|
private fun saveFile(fileContent: String, suggestedName: String) {
|
|
tmpFileContentToSave = fileContent
|
|
|
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
addCategory(Intent.CATEGORY_OPENABLE)
|
|
type = "text/*"
|
|
putExtra(Intent.EXTRA_TITLE, suggestedName)
|
|
}
|
|
|
|
startActivityForResult(intent, CREATE_FILE_ACTION_CODE)
|
|
}
|
|
|
|
override fun getSystemService(name: String): Any? {
|
|
return if (Build.VERSION.SDK_INT >= 29 && name == "clipboard") {
|
|
// QT will always attempt to read the clipboard if content is there.
|
|
// since we have no use of the clipboard in android 10+
|
|
// we _can_ return null
|
|
// And we definitely should since android 12 displays clipboard access.
|
|
null
|
|
} else {
|
|
super.getSystemService(name)
|
|
}
|
|
}
|
|
|
|
external fun handleBackButton(): Boolean
|
|
|
|
external fun onServiceMessage(actionCode: Int, body: String?)
|
|
external fun qtOnServiceConnected()
|
|
external fun qtOnServiceDisconnected()
|
|
external fun onActivityMessage(actionCode: Int, body: String?)
|
|
|
|
private fun dispatchParcel(actionCode: Int, body: String) {
|
|
if (!isBound) {
|
|
Log.d(TAG, "dispatchParcel: not bound")
|
|
delayedCommands.add(Pair(actionCode, body))
|
|
return
|
|
}
|
|
|
|
if (delayedCommands.size > 0) {
|
|
for (command in delayedCommands) {
|
|
processCommand(command.first, command.second)
|
|
}
|
|
|
|
delayedCommands.clear()
|
|
}
|
|
|
|
processCommand(actionCode, body)
|
|
}
|
|
|
|
private fun processCommand(actionCode: Int, body: String) {
|
|
val out: Parcel = Parcel.obtain()
|
|
out.writeByteArray(body.toByteArray())
|
|
|
|
try {
|
|
vpnServiceBinder?.transact(actionCode, out, Parcel.obtain(), 0)
|
|
} catch (e: DeadObjectException) {
|
|
isBound = false
|
|
vpnServiceBinder = null
|
|
qtOnServiceDisconnected()
|
|
} catch (e: RemoteException) {
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(newIntent: Intent) {
|
|
super.onNewIntent(intent)
|
|
|
|
setIntent(newIntent)
|
|
|
|
val newIntentAction = newIntent.action
|
|
|
|
if (newIntent != null && newIntentAction != null && newIntentAction == INTENT_ACTION_IMPORT_CONFIG) {
|
|
configString = newIntent.getStringExtra("CONFIG")
|
|
}
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
|
|
if (configString != null && isBound) {
|
|
sendImportConfigCommand()
|
|
}
|
|
}
|
|
|
|
private fun sendImportConfigCommand() {
|
|
if (configString != null) {
|
|
val msg: Parcel = Parcel.obtain()
|
|
msg.writeString(configString!!)
|
|
|
|
try {
|
|
vpnServiceBinder?.transact(ACTION_IMPORT_CONFIG, msg, Parcel.obtain(), 0)
|
|
} catch (e: RemoteException) {
|
|
e.printStackTrace()
|
|
}
|
|
|
|
configString = null
|
|
}
|
|
}
|
|
|
|
private fun createConnection() = object : ServiceConnection {
|
|
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
|
vpnServiceBinder = binder
|
|
|
|
// This is called when the connection with the service has been
|
|
// established, giving us the object we can use to
|
|
// interact with the service. We are communicating with the
|
|
// service using a Messenger, so here we get a client-side
|
|
// representation of that from the raw IBinder object.
|
|
if (registerBinder()){
|
|
qtOnServiceConnected();
|
|
} else {
|
|
qtOnServiceDisconnected();
|
|
return
|
|
}
|
|
|
|
isBound = true
|
|
}
|
|
|
|
override fun onServiceDisconnected(className: ComponentName) {
|
|
vpnServiceBinder = null
|
|
isBound = false
|
|
qtOnServiceDisconnected();
|
|
}
|
|
}
|
|
|
|
private var connection: ServiceConnection = createConnection()
|
|
|
|
private fun registerBinder(): Boolean {
|
|
val binder = VPNClientBinder()
|
|
val out: Parcel = Parcel.obtain()
|
|
out.writeStrongBinder(binder)
|
|
|
|
try {
|
|
// Register our IBinder Listener
|
|
vpnServiceBinder?.transact(ACTION_REGISTER_LISTENER, out, Parcel.obtain(), 0)
|
|
return true
|
|
} catch (e: DeadObjectException) {
|
|
isBound = false
|
|
vpnServiceBinder = null
|
|
} catch (e: RemoteException) {
|
|
e.printStackTrace()
|
|
}
|
|
return false
|
|
}
|
|
|
|
private fun initServiceConnection() {
|
|
// We already have a connection to the service,
|
|
// just need to re-register the binder
|
|
if (isBound && vpnServiceBinder!!.isBinderAlive() && registerBinder()) {
|
|
qtOnServiceConnected()
|
|
return
|
|
}
|
|
|
|
bindService(Intent(this, VPNService::class.java), connection, Context.BIND_AUTO_CREATE)
|
|
}
|
|
|
|
// TODO: Move all ipc codes into a shared lib.
|
|
// this is getting out of hand.
|
|
private val PERMISSION_TRANSACTION = 1337
|
|
private val ACTION_REGISTER_LISTENER = 3
|
|
private val ACTION_RESUME_ACTIVATE = 7
|
|
private val ACTION_IMPORT_CONFIG = 11
|
|
private val EVENT_PERMISSION_REQUIRED = 6
|
|
private val EVENT_DISCONNECTED = 2
|
|
|
|
private val UI_EVENT_QR_CODE_RECEIVED = 0
|
|
|
|
fun onPermissionRequest(code: Int, data: Parcel?) {
|
|
if (code != EVENT_PERMISSION_REQUIRED) {
|
|
return
|
|
}
|
|
|
|
val x = Intent()
|
|
x.readFromParcel(data)
|
|
|
|
startActivityForResult(x, PERMISSION_TRANSACTION)
|
|
}
|
|
|
|
override protected fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
if (requestCode == PERMISSION_TRANSACTION) {
|
|
// THATS US!
|
|
if (resultCode == RESULT_OK) {
|
|
// Prompt accepted, tell service to retry.
|
|
dispatchParcel(ACTION_RESUME_ACTIVATE, "")
|
|
} else {
|
|
// Tell the Client we've disconnected
|
|
onServiceMessage(EVENT_DISCONNECTED, "")
|
|
}
|
|
return
|
|
}
|
|
|
|
super.onActivityResult(requestCode, resultCode, data)
|
|
|
|
if (requestCode == CAMERA_ACTION_CODE && resultCode == RESULT_OK) {
|
|
val extra = data?.getStringExtra("result") ?: ""
|
|
onActivityMessage(UI_EVENT_QR_CODE_RECEIVED, extra)
|
|
}
|
|
|
|
if (requestCode == CREATE_FILE_ACTION_CODE && resultCode == RESULT_OK) {
|
|
data?.data?.also { uri ->
|
|
alterDocument(uri)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun alterDocument(uri: Uri) {
|
|
try {
|
|
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { fd ->
|
|
FileOutputStream(fd.fileDescriptor).use { fos ->
|
|
fos.write(tmpFileContentToSave.toByteArray())
|
|
}
|
|
}
|
|
} catch (e: FileNotFoundException) {
|
|
e.printStackTrace()
|
|
} catch (e: IOException) {
|
|
e.printStackTrace()
|
|
}
|
|
|
|
tmpFileContentToSave = ""
|
|
}
|
|
|
|
private fun putToClipboard(text: String) {
|
|
this.runOnUiThread {
|
|
val clipboard = applicationContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager?
|
|
|
|
if (clipboard != null) {
|
|
val clip: ClipData = ClipData.newPlainText("", text)
|
|
clipboard.setPrimaryClip(clip)
|
|
}
|
|
}
|
|
}
|
|
}
|