Refactor import config
Remove the path filter, as the content path may not contain a filename. Disable import when viewing files. Config can be imported from: - shared file - shared text - vpn:// link
This commit is contained in:
parent
1576aed1ea
commit
195bdb947e
7 changed files with 109 additions and 171 deletions
|
|
@ -17,8 +17,6 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- To request network state for android < 31 -->
|
<!-- To request network state for android < 31 -->
|
||||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
@ -78,57 +76,26 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ImportConfigActivity"
|
android:name=".ImportConfigActivity"
|
||||||
android:exported="true">
|
android:excludeFromRecents="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Translucent">
|
||||||
|
|
||||||
<intent-filter android:label="AmneziaVPN">
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:scheme="file" />
|
<data android:mimeType="application/octet-stream" />
|
||||||
<data android:scheme="content" />
|
<data android:mimeType="text/plain" />
|
||||||
<data android:mimeType="*/*" />
|
|
||||||
<data android:host="*" />
|
|
||||||
<data android:pathPattern=".*\\.vpn" />
|
|
||||||
<data android:pathPattern=".*\\..*\\.vpn" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\.vpn" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.vpn" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.vpn" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter android:label="AmneziaVPN">
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data android:scheme="file" />
|
<data android:scheme="vpn" android:host="*" />
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:mimeType="*/*" />
|
|
||||||
<data android:host="*" />
|
|
||||||
<data android:pathPattern=".*\\.cfg" />
|
|
||||||
<data android:pathPattern=".*\\..*\\.cfg" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\.cfg" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.cfg" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.cfg" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter android:label="AmneziaVPN">
|
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
|
|
||||||
<data android:scheme="file" />
|
|
||||||
<data android:scheme="content" />
|
|
||||||
<data android:mimeType="*/*" />
|
|
||||||
<data android:host="*" />
|
|
||||||
<data android:pathPattern=".*\\.conf" />
|
|
||||||
<data android:pathPattern=".*\\..*\\.conf" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\.conf" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.conf" />
|
|
||||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.conf" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?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" />
|
|
||||||
|
|
@ -2,6 +2,7 @@ package org.amnezia.vpn
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.net.VpnService
|
import android.net.VpnService
|
||||||
|
|
@ -140,12 +141,33 @@ class AmneziaActivity : QtActivity() {
|
||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.v(TAG, "Create Amnezia activity")
|
Log.v(TAG, "Create Amnezia activity: $intent")
|
||||||
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||||
vpnServiceMessenger = IpcMessenger(
|
vpnServiceMessenger = IpcMessenger(
|
||||||
onDeadObjectException = ::doUnbindService,
|
onDeadObjectException = ::doUnbindService,
|
||||||
messengerName = "VpnService"
|
messengerName = "VpnService"
|
||||||
)
|
)
|
||||||
|
intent?.let(::processIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
Log.v(TAG, "onNewIntent: $intent")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -1,137 +1,88 @@
|
||||||
package org.amnezia.vpn
|
package org.amnezia.vpn
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.Intent.ACTION_SEND
|
||||||
|
import android.content.Intent.ACTION_VIEW
|
||||||
|
import android.content.Intent.CATEGORY_DEFAULT
|
||||||
|
import android.content.Intent.EXTRA_TEXT
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build.VERSION
|
||||||
|
import android.os.Build.VERSION_CODES
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import androidx.activity.ComponentActivity
|
||||||
import android.widget.Toast
|
import java.io.BufferedReader
|
||||||
import androidx.core.app.ActivityCompat
|
import org.amnezia.vpn.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
|
|
||||||
import java.io.*
|
private const val TAG = "ImportConfigActivity"
|
||||||
|
|
||||||
const val INTENT_ACTION_IMPORT_CONFIG = "org.amnezia.vpn.IMPORT_CONFIG"
|
const val ACTION_IMPORT_CONFIG = "org.amnezia.vpn.IMPORT_CONFIG"
|
||||||
|
const val EXTRA_CONFIG = "CONFIG"
|
||||||
|
|
||||||
class ImportConfigActivity : Activity() {
|
class ImportConfigActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val STORAGE_PERMISSION_CODE = 42
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_import_config)
|
Log.v(TAG, "Create Import Config Activity: $intent")
|
||||||
startReadConfig(intent)
|
intent?.let(::readConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
startReadConfig(intent)
|
Log.v(TAG, "onNewIntent: $intent")
|
||||||
|
intent?.let(::readConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startMainActivity(config: String?) {
|
private fun readConfig(intent: Intent) {
|
||||||
|
when (intent.action) {
|
||||||
|
ACTION_SEND -> {
|
||||||
|
Log.v(TAG, "Process SEND action, type: ${intent.type}")
|
||||||
|
when (intent.type) {
|
||||||
|
"application/octet-stream" ->
|
||||||
|
processStream(intent)
|
||||||
|
|
||||||
if (config == null || config.length == 0) {
|
"text/plain" -> {
|
||||||
return
|
intent.getStringExtra(EXTRA_TEXT)?.let(::startMainActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION_VIEW -> {
|
||||||
|
Log.v(TAG, "Process VIEW action")
|
||||||
|
intent.data?.toString()?.let(::startMainActivity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val activityIntent = Intent(applicationContext, AmneziaActivity::class.java)
|
|
||||||
activityIntent.action = INTENT_ACTION_IMPORT_CONFIG
|
|
||||||
activityIntent.addCategory("android.intent.category.DEFAULT")
|
|
||||||
activityIntent.putExtra("CONFIG", config)
|
|
||||||
|
|
||||||
startActivity(activityIntent)
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startReadConfig(intent: Intent?) {
|
private fun processStream(intent: Intent) {
|
||||||
val newIntent = intent
|
getUriCompat(intent)?.let { uri ->
|
||||||
val newIntentAction: String = newIntent?.action ?: ""
|
contentResolver.openInputStream(uri)?.use {
|
||||||
|
it.bufferedReader().use(BufferedReader::readText).let(::startMainActivity)
|
||||||
if (newIntent != null && newIntentAction == Intent.ACTION_VIEW) {
|
}
|
||||||
readConfig(newIntent, newIntentAction)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readConfig(newIntent: Intent, newIntentAction: String) {
|
private fun getUriCompat(intent: Intent): Uri? =
|
||||||
if (isReadStorageAllowed()) {
|
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||||
val configString = processIntent(newIntent, newIntentAction)
|
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
startMainActivity(configString)
|
|
||||||
} else {
|
} else {
|
||||||
requestStoragePermission()
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestStoragePermission() {
|
private fun startMainActivity(config: String) {
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_CODE)
|
if (config.isNotBlank()) {
|
||||||
}
|
Log.v(TAG, "startMainActivity")
|
||||||
|
Intent(applicationContext, AmneziaActivity::class.java).apply {
|
||||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
|
action = ACTION_IMPORT_CONFIG
|
||||||
if (requestCode == STORAGE_PERMISSION_CODE) {
|
addCategory(CATEGORY_DEFAULT)
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
putExtra(EXTRA_CONFIG, config)
|
||||||
val configString = processIntent(intent, intent.action!!)
|
flags = FLAG_ACTIVITY_NEW_TASK
|
||||||
|
}.also {
|
||||||
if (configString != null) {
|
startActivity(it)
|
||||||
startMainActivity(configString)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Oops you just denied the permission", Toast.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ object QtAndroidController {
|
||||||
external fun onVpnReconnecting()
|
external fun onVpnReconnecting()
|
||||||
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
|
||||||
|
|
||||||
external fun onConfigImported()
|
external fun onConfigImported(data: String)
|
||||||
|
|
||||||
external fun decodeQrCode(data: String): Boolean
|
external fun decodeQrCode(data: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -77,14 +77,9 @@ AndroidController::AndroidController() : QObject()
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
this, &AndroidController::configImported, this,
|
this, &AndroidController::configImported, this,
|
||||||
[]() {
|
[this](const QString& config) {
|
||||||
// todo: not yet implemented
|
qDebug() << "Android event: config import";
|
||||||
qDebug() << "Transact: config import";
|
emit importConfigFromOutside(config);
|
||||||
/*auto doc = QJsonDocument::fromJson(parcelBody.toUtf8());
|
|
||||||
|
|
||||||
QString buffer = doc.object()["config"].toString();
|
|
||||||
qDebug() << "Transact: config string" << buffer;
|
|
||||||
importConfigFromOutside(buffer);*/
|
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +106,7 @@ bool AndroidController::initialize()
|
||||||
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
|
{"onVpnDisconnected", "()V", reinterpret_cast<void *>(onVpnDisconnected)},
|
||||||
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
|
{"onVpnReconnecting", "()V", reinterpret_cast<void *>(onVpnReconnecting)},
|
||||||
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
|
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
|
||||||
{"onConfigImported", "()V", reinterpret_cast<void *>(onConfigImported)},
|
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
|
||||||
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
|
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -290,12 +285,20 @@ void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBy
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
void AndroidController::onConfigImported(JNIEnv *env, jobject thiz)
|
void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
|
||||||
{
|
{
|
||||||
Q_UNUSED(env);
|
Q_UNUSED(env);
|
||||||
Q_UNUSED(thiz);
|
Q_UNUSED(thiz);
|
||||||
|
|
||||||
emit AndroidController::instance()->configImported();
|
const char *buffer = env->GetStringUTFChars(data, nullptr);
|
||||||
|
if (!buffer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString config(buffer);
|
||||||
|
env->ReleaseStringUTFChars(data, buffer);
|
||||||
|
|
||||||
|
emit AndroidController::instance()->configImported(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ signals:
|
||||||
void vpnDisconnected();
|
void vpnDisconnected();
|
||||||
void vpnReconnecting();
|
void vpnReconnecting();
|
||||||
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
|
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
|
||||||
void configImported();
|
void configImported(QString config);
|
||||||
void importConfigFromOutside(QString &data);
|
void importConfigFromOutside(QString config);
|
||||||
void initConnectionState(Vpn::ConnectionState state);
|
void initConnectionState(Vpn::ConnectionState state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
@ -64,7 +64,7 @@ private:
|
||||||
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
|
static void onVpnDisconnected(JNIEnv *env, jobject thiz);
|
||||||
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
|
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
|
||||||
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
|
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
|
||||||
static void onConfigImported(JNIEnv *env, jobject thiz);
|
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
|
||||||
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
|
||||||
|
|
||||||
template <typename Ret, typename ...Args>
|
template <typename Ret, typename ...Args>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue