Merge pull request #204 from amnezia-vpn/fix/android_config_import

Android config import
This commit is contained in:
pokamest 2023-03-29 22:16:16 +01:00 committed by GitHub
commit 449af8060a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 256 additions and 154 deletions

View file

@ -52,6 +52,37 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="org.amnezia.vpn.qt.IMPORT_CONFIG" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
<meta-data
android:name="android.app.background_running"
android:value="false"/>
<meta-data
android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
</activity>
<activity
android:name=".qt.CameraActivity"
android:exported="false" />
<activity
android:name=".qt.ImportConfigActivity"
android:exported="true" >
<intent-filter android:label="AmneziaVPN"> <intent-filter android:label="AmneziaVPN">
<action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
@ -96,29 +127,8 @@
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/> <data android:pathPattern=".*\\..*\\..*\\..*\\.conf"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/> <data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf"/>
</intent-filter> </intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
<meta-data
android:name="android.app.extract_android_style"
android:value="minimal" />
<meta-data
android:name="android.app.background_running"
android:value="false"/>
<meta-data
android:name="android.app.arguments"
android:value="-- %%INSERT_APP_ARGUMENTS%% --" />
</activity> </activity>
<activity
android:name=".qt.CameraActivity"
android:exported="false" />
<service <service
android:name=".VPNService" android:name=".VPNService"
android:process=":QtOnlyProcess" android:process=":QtOnlyProcess"

View file

@ -75,7 +75,7 @@ android {
* The following variables: * The following variables:
* - androidBuildToolsVersion, * - androidBuildToolsVersion,
* - androidCompileSdkVersion * - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files * - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application * needed to build any Qt application
* on Android. * on Android.
* *
@ -106,7 +106,7 @@ android {
renderscript.srcDirs = ['src'] renderscript.srcDirs = ['src']
assets.srcDirs = ['assets'] assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs'] jniLibs.srcDirs = ['libs']
androidTest.assets.srcDirs += files("${qt5AndroidDir}/schemas".toString()) androidTest.assets.srcDirs += files("${qtAndroidDir}/schemas".toString())
} }
} }
@ -140,7 +140,7 @@ android {
versionName "2.0.10" // Change to a higher number versionName "2.0.10" // Change to a higher number
javaCompileOptions.annotationProcessorOptions.arguments = [ javaCompileOptions.annotationProcessorOptions.arguments = [
"room.schemaLocation": "${qt5AndroidDir}/schemas".toString() "room.schemaLocation": "${qtAndroidDir}/schemas".toString()
] ]
} }

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View file

@ -0,0 +1,140 @@
package org.amnezia.vpn.qt
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.io.*
import org.amnezia.vpn.R
const val INTENT_ACTION_IMPORT_CONFIG = "org.amnezia.vpn.qt.IMPORT_CONFIG"
class ImportConfigActivity : Activity() {
private val STORAGE_PERMISSION_CODE = 42
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_import_config)
startReadConfig(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
startReadConfig(intent)
}
private fun startMainActivity(config: String?) {
if (config == null || config.length == 0) {
return
}
val activityIntent = Intent(applicationContext, VPNActivity::class.java)
activityIntent.action = INTENT_ACTION_IMPORT_CONFIG
activityIntent.addCategory("android.intent.category.DEFAULT")
activityIntent.putExtra("CONFIG", config)
startActivity(activityIntent)
finish()
}
private fun startReadConfig(intent: Intent?) {
val newIntent = intent
val newIntentAction: String = newIntent?.action ?: ""
if (newIntent != null && newIntentAction == Intent.ACTION_VIEW) {
readConfig(newIntent, newIntentAction)
}
}
private fun readConfig(newIntent: Intent, newIntentAction: String) {
if (isReadStorageAllowed()) {
val configString = processIntent(newIntent, newIntentAction)
startMainActivity(configString)
} else {
requestStoragePermission()
}
}
private fun requestStoragePermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
val configString = processIntent(intent, intent.action!!)
if (configString != null) {
startMainActivity(configString)
}
} else {
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
}
}
}
private fun processIntent(intent: Intent, action: String): String? {
val scheme = intent.scheme
if (scheme == null) {
return null
}
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
val resolver = contentResolver
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
val uri = intent.data
val name: String? = getContentName(resolver, uri)
println("Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri!!)
return input?.bufferedReader()?.use(BufferedReader::readText)
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
val uri = intent.data
val name = uri!!.lastPathSegment
println("File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri)
return input?.bufferedReader()?.use(BufferedReader::readText)
}
}
return null
}
private fun isReadStorageAllowed(): Boolean {
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
return permissionStatus == PackageManager.PERMISSION_GRANTED
}
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
val cursor = resolver!!.query(uri!!, null, null, null, null)
cursor.use {
cursor!!.moveToFirst()
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
return if (nameIndex >= 0) {
return cursor.getString(nameIndex)
} else {
null
}
}
}
}

View file

@ -34,15 +34,23 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private var configString: String? = null private var configString: String? = null
private var vpnServiceBinder: IBinder? = null private var vpnServiceBinder: IBinder? = null
private var isBound = false private var isBound = false
set(value) {
field = value
if (value && configString != null) {
sendImportConfigCommand()
}
}
private val TAG = "VPNActivity" private val TAG = "VPNActivity"
private val STORAGE_PERMISSION_CODE = 42
private val CAMERA_ACTION_CODE = 101 private val CAMERA_ACTION_CODE = 101
private val CREATE_FILE_ACTION_CODE = 102 private val CREATE_FILE_ACTION_CODE = 102
private var tmpFileContentToSave: String = "" private var tmpFileContentToSave: String = ""
private val delayedCommands: ArrayList<Pair<Int, String>> = ArrayList()
companion object { companion object {
private lateinit var instance: VPNActivity private lateinit var instance: VPNActivity
@ -72,16 +80,16 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
val newIntent = intent
val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null) {
configString = processIntent(newIntent, newIntentAction)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
instance = this 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() { private fun startQrCodeActivity() {
@ -123,9 +131,22 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
private fun dispatchParcel(actionCode: Int, body: String) { private fun dispatchParcel(actionCode: Int, body: String) {
if (!isBound) { if (!isBound) {
Log.d(TAG, "dispatchParcel: not bound") Log.d(TAG, "dispatchParcel: not bound")
delayedCommands.add(Pair(actionCode, body))
return 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() val out: Parcel = Parcel.obtain()
out.writeByteArray(body.toByteArray()) out.writeByteArray(body.toByteArray())
@ -141,19 +162,15 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
override fun onNewIntent(newIntent: Intent) { override fun onNewIntent(newIntent: Intent) {
intent = newIntent super.onNewIntent(intent)
setIntent(newIntent)
val newIntentAction = newIntent.action val newIntentAction = newIntent.action
if (newIntent != null && newIntentAction != null && newIntentAction != Intent.ACTION_MAIN) { if (newIntent != null && newIntentAction != null && newIntentAction == INTENT_ACTION_IMPORT_CONFIG) {
if (isReadStorageAllowed()) { configString = newIntent.getStringExtra("CONFIG")
configString = processIntent(newIntent, newIntentAction)
} else {
requestStoragePermission()
}
} }
super.onNewIntent(intent)
} }
override fun onResume() { override fun onResume() {
@ -164,84 +181,6 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
} }
private fun isReadStorageAllowed(): Boolean {
val permissionStatus = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
return permissionStatus == PackageManager.PERMISSION_GRANTED
}
private fun requestStoragePermission() {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Storage read permission granted")
if (configString == null) {
configString = processIntent(intent, intent.action!!)
}
if (configString != null) {
Log.d(TAG, "not empty")
sendImportConfigCommand()
} else {
Log.d(TAG, "empty")
}
} else {
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
}
}
}
private fun processIntent(intent: Intent, action: String): String? {
val scheme = intent.scheme
if (scheme == null) {
return null
}
if (action.compareTo(Intent.ACTION_VIEW) == 0) {
val resolver = contentResolver
if (scheme.compareTo(ContentResolver.SCHEME_CONTENT) == 0) {
val uri = intent.data
val name: String? = getContentName(resolver, uri)
Log.d(TAG, "Content intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri!!)
return input?.bufferedReader()?.use(BufferedReader::readText)
} else if (scheme.compareTo(ContentResolver.SCHEME_FILE) == 0) {
val uri = intent.data
val name = uri!!.lastPathSegment
Log.d(TAG, "File intent detected: " + action + " : " + intent.dataString + " : " + intent.type + " : " + name)
val input = resolver.openInputStream(uri)
return input?.bufferedReader()?.use(BufferedReader::readText)
}
}
return null
}
private fun getContentName(resolver: ContentResolver?, uri: Uri?): String? {
val cursor = resolver!!.query(uri!!, null, null, null, null)
cursor.use {
cursor!!.moveToFirst()
val nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
return if (nameIndex >= 0) {
return cursor.getString(nameIndex)
} else {
null
}
}
}
private fun sendImportConfigCommand() { private fun sendImportConfigCommand() {
if (configString != null) { if (configString != null) {
val msg: Parcel = Parcel.obtain() val msg: Parcel = Parcel.obtain()
@ -257,7 +196,7 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
} }
private var connection: ServiceConnection = object : ServiceConnection { private fun createConnection() = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) { override fun onServiceConnected(className: ComponentName, binder: IBinder) {
vpnServiceBinder = binder vpnServiceBinder = binder
@ -283,6 +222,8 @@ class VPNActivity : org.qtproject.qt.android.bindings.QtActivity() {
} }
} }
private var connection: ServiceConnection = createConnection()
private fun registerBinder(): Boolean { private fun registerBinder(): Boolean {
val binder = VPNClientBinder() val binder = VPNClientBinder()
val out: Parcel = Parcel.obtain() val out: Parcel = Parcel.obtain()

View file

@ -92,7 +92,6 @@ AndroidController::AndroidController() : QObject()
connect(activity, &AndroidVPNActivity::eventStatisticUpdate, this, connect(activity, &AndroidVPNActivity::eventStatisticUpdate, this,
[this](const QString& parcelBody) { [this](const QString& parcelBody) {
qDebug() << "Transact: update"; qDebug() << "Transact: update";
auto doc = QJsonDocument::fromJson(parcelBody.toUtf8()); auto doc = QJsonDocument::fromJson(parcelBody.toUtf8());
QString rx = doc.object()["rx_bytes"].toString(); QString rx = doc.object()["rx_bytes"].toString();
@ -250,7 +249,7 @@ void AndroidController::cleanupBackendLogs() {
} }
void AndroidController::importConfig(const QString& data){ void AndroidController::importConfig(const QString& data){
m_startPageLogic->importConnectionFromCode(data); m_startPageLogic->selectConfigFormat(data);
} }
const QJsonObject &AndroidController::vpnConfig() const const QJsonObject &AndroidController::vpnConfig() const

View file

@ -175,14 +175,7 @@ void StartPageLogic::onPushButtonImportOpenFile()
file.open(QIODevice::ReadOnly); file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll(); QByteArray data = file.readAll();
auto configFormat = checkConfigFormat(QString(data)); selectConfigFormat(QString(data));
if (configFormat == ConfigTypes::OpenVpn) {
importConnectionFromOpenVpnConfig(QString(data));
} else if (configFormat == ConfigTypes::WireGuard) {
importConnectionFromWireguardConfig(QString(data));
} else {
importConnectionFromCode(QString(data));
}
} }
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
@ -192,6 +185,18 @@ void StartPageLogic::startQrDecoder()
} }
#endif #endif
void StartPageLogic::selectConfigFormat(QString configData)
{
auto configFormat = checkConfigFormat(configData);
if (configFormat == ConfigTypes::OpenVpn) {
importConnectionFromOpenVpnConfig(configData);
} else if (configFormat == ConfigTypes::WireGuard) {
importConnectionFromWireguardConfig(configData);
} else {
importConnectionFromCode(configData);
}
}
bool StartPageLogic::importConnection(const QJsonObject &profile) bool StartPageLogic::importConnection(const QJsonObject &profile)
{ {
ServerCredentials credentials; ServerCredentials credentials;

View file

@ -35,6 +35,8 @@ public:
Q_INVOKABLE void startQrDecoder(); Q_INVOKABLE void startQrDecoder();
#endif #endif
void selectConfigFormat(QString configData);
bool importConnection(const QJsonObject &profile); bool importConnection(const QJsonObject &profile);
bool importConnectionFromCode(QString code); bool importConnectionFromCode(QString code);
bool importConnectionFromQr(const QByteArray &data); bool importConnectionFromQr(const QByteArray &data);