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:
albexk 2023-12-11 22:56:01 +03:00
parent 1576aed1ea
commit 195bdb947e
7 changed files with 109 additions and 171 deletions

View file

@ -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>

View file

@ -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" />

View file

@ -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() {

View file

@ -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()
}
}
}
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
} }
} }
finish()
} }
} }

View file

@ -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
} }

View file

@ -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

View file

@ -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>