diff --git a/client/android/AndroidManifest.xml b/client/android/AndroidManifest.xml
index 30b77f09..cf237d07 100644
--- a/client/android/AndroidManifest.xml
+++ b/client/android/AndroidManifest.xml
@@ -22,7 +22,6 @@
-
diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
index 11497274..0c0ec0f9 100644
--- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
+++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt
@@ -2,6 +2,7 @@ package org.amnezia.vpn
import android.content.ComponentName
import android.content.Intent
+import android.content.Intent.EXTRA_MIME_TYPES
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.ServiceConnection
import android.net.Uri
@@ -12,11 +13,13 @@ import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
+import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE
+import kotlin.text.RegexOption.IGNORE_CASE
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -35,6 +38,7 @@ private const val TAG = "AmneziaActivity"
private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
+private const val OPEN_FILE_ACTION_CODE = 3
private const val BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() {
@@ -201,6 +205,15 @@ class AmneziaActivity : QtActivity() {
}
}
+ OPEN_FILE_ACTION_CODE -> {
+ when (resultCode) {
+ RESULT_OK -> data?.data?.toString() ?: ""
+ else -> ""
+ }.let { uri ->
+ QtAndroidController.onFileOpened(uri)
+ }
+ }
+
CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) {
RESULT_OK -> {
@@ -370,6 +383,36 @@ class AmneziaActivity : QtActivity() {
}
}
+ @Suppress("unused")
+ fun openFile(filter: String?) {
+ Log.v(TAG, "Open file with filter: $filter")
+
+ val mimeTypes = if (!filter.isNullOrEmpty()) {
+ val extensionRegex = "\\*\\.[a-z .]+".toRegex(IGNORE_CASE)
+ val mime = MimeTypeMap.getSingleton()
+ extensionRegex.findAll(filter).map {
+ mime.getMimeTypeFromExtension(it.value.drop(2))
+ }.filterNotNull().toSet()
+ } else emptySet()
+
+ Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ Log.d(TAG, "File mimyType filter: $mimeTypes")
+ when (mimeTypes.size) {
+ 1 -> type = mimeTypes.first()
+
+ in 2..Int.MAX_VALUE -> {
+ type = "*/*"
+ putExtra(EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
+ }
+
+ else -> type = "*/*"
+ }
+ }.also {
+ startActivityForResult(it, OPEN_FILE_ACTION_CODE)
+ }
+ }
+
@Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text")
diff --git a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
index bc8cc425..cab810a7 100644
--- a/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
+++ b/client/android/src/org/amnezia/vpn/qt/QtAndroidController.kt
@@ -15,6 +15,8 @@ object QtAndroidController {
external fun onVpnReconnecting()
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
+ external fun onFileOpened(uri: String)
+
external fun onConfigImported(data: String)
external fun decodeQrCode(data: String): Boolean
diff --git a/client/cmake/android.cmake b/client/cmake/android.cmake
index 2d08b4b6..7ffa680e 100644
--- a/client/cmake/android.cmake
+++ b/client/cmake/android.cmake
@@ -27,7 +27,7 @@ link_directories(${CMAKE_CURRENT_SOURCE_DIR}/platforms/android)
set(HEADERS ${HEADERS}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.h
- ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.h
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.h
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.h
)
@@ -35,7 +35,7 @@ set(HEADERS ${HEADERS}
set(SOURCES ${SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_controller.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_notificationhandler.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/androidutils.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/android_utils.cpp
${CMAKE_CURRENT_SOURCE_DIR}/platforms/android/authResultReceiver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/protocols/android_vpnprotocol.cpp
)
diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp
index a739bee3..225ceebe 100644
--- a/client/platforms/android/android_controller.cpp
+++ b/client/platforms/android/android_controller.cpp
@@ -1,8 +1,10 @@
-#include
#include
#include
+#include
+#include
#include "android_controller.h"
+#include "android_utils.h"
#include "ui/controllers/importController.h"
namespace
@@ -106,6 +108,7 @@ bool AndroidController::initialize()
{"onVpnDisconnected", "()V", reinterpret_cast(onVpnDisconnected)},
{"onVpnReconnecting", "()V", reinterpret_cast(onVpnReconnecting)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast(onStatisticsUpdate)},
+ {"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast(onConfigImported)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast(decodeQrCode)}
};
@@ -127,7 +130,7 @@ auto AndroidController::callActivityMethod(const char *methodName, const char *s
const std::function &defValue, Args &&...args)
{
qDebug() << "Call activity method:" << methodName;
- QJniObject activity = QNativeInterface::QAndroidApplication::context();
+ QJniObject activity = AndroidUtils::getActivity();
if (activity.isValid()) {
return activity.callMethod(methodName, signature, std::forward(args)...);
} else {
@@ -165,6 +168,24 @@ void AndroidController::saveFile(const QString &fileName, const QString &data)
QJniObject::fromString(data).object());
}
+QString AndroidController::openFile(const QString &filter)
+{
+ QEventLoop wait;
+ QString fileName;
+ connect(this, &AndroidController::fileOpened, this,
+ [&fileName, &wait](const QString &uri) {
+ qDebug() << "Android event: file opened; uri:" << uri;
+ fileName = QQmlFile::urlToLocalFileOrQrc(uri);
+ qDebug() << "Android opened filename:" << fileName;
+ wait.quit();
+ },
+ static_cast(Qt::QueuedConnection | Qt::SingleShotConnection));
+ callActivityMethod("openFile", "(Ljava/lang/String;)V",
+ QJniObject::fromString(filter).object());
+ wait.exec();
+ return fileName;
+}
+
void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec)
{
callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V",
@@ -285,20 +306,19 @@ void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBy
}
// static
-void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
+void AndroidController::onFileOpened(JNIEnv *env, jobject thiz, jstring uri)
{
- Q_UNUSED(env);
Q_UNUSED(thiz);
- const char *buffer = env->GetStringUTFChars(data, nullptr);
- if (!buffer) {
- return;
- }
+ emit AndroidController::instance()->fileOpened(AndroidUtils::convertJString(env, uri));
+}
- QString config(buffer);
- env->ReleaseStringUTFChars(data, buffer);
+// static
+void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
+{
+ Q_UNUSED(thiz);
- emit AndroidController::instance()->configImported(config);
+ emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data));
}
// static
@@ -306,12 +326,5 @@ bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{
Q_UNUSED(thiz);
- const char *buffer = env->GetStringUTFChars(data, nullptr);
- if (!buffer) {
- return false;
- }
-
- QString code(buffer);
- env->ReleaseStringUTFChars(data, buffer);
- return ImportController::decodeQrCode(code);
+ return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data));
}
diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h
index 4e72cbdf..481f4b49 100644
--- a/client/platforms/android/android_controller.h
+++ b/client/platforms/android/android_controller.h
@@ -18,7 +18,8 @@ public:
bool initialize();
// keep synchronized with org.amnezia.vpn.protocol.ProtocolState
- enum class ConnectionState {
+ enum class ConnectionState
+ {
CONNECTED,
CONNECTING,
DISCONNECTED,
@@ -30,7 +31,8 @@ public:
ErrorCode start(const QJsonObject &vpnConfig);
void stop();
void setNotificationText(const QString &title, const QString &message, int timerSec);
- void saveFile(const QString& fileName, const QString &data);
+ void saveFile(const QString &fileName, const QString &data);
+ QString openFile(const QString &filter);
void startQrReaderActivity();
signals:
@@ -43,6 +45,7 @@ signals:
void vpnDisconnected();
void vpnReconnecting();
void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
+ void fileOpened(QString uri);
void configImported(QString config);
void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state);
@@ -65,6 +68,7 @@ private:
static void onVpnReconnecting(JNIEnv *env, jobject thiz);
static void onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes);
static void onConfigImported(JNIEnv *env, jobject thiz, jstring data);
+ static void onFileOpened(JNIEnv *env, jobject thiz, jstring uri);
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
template
diff --git a/client/platforms/android/android_utils.cpp b/client/platforms/android/android_utils.cpp
new file mode 100644
index 00000000..4a994ab0
--- /dev/null
+++ b/client/platforms/android/android_utils.cpp
@@ -0,0 +1,30 @@
+#include
+#include "android_utils.h"
+
+namespace AndroidUtils
+{
+
+QJniObject getActivity()
+{
+ return QNativeInterface::QAndroidApplication::context();
+}
+
+QString convertJString(JNIEnv *env, jstring data)
+{
+ int len = env->GetStringLength(data);
+ QString res(len, Qt::Uninitialized);
+ env->GetStringRegion(data, 0, len, reinterpret_cast(res.data()));
+ return res;
+}
+
+void runOnAndroidThreadSync(const std::function &runnable)
+{
+ QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable).waitForFinished();
+}
+
+void runOnAndroidThreadAsync(const std::function &runnable)
+{
+ QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable);
+}
+
+}
diff --git a/client/platforms/android/android_utils.h b/client/platforms/android/android_utils.h
new file mode 100644
index 00000000..9ed58b75
--- /dev/null
+++ b/client/platforms/android/android_utils.h
@@ -0,0 +1,16 @@
+#ifndef ANDROID_UTILS_H
+#define ANDROID_UTILS_H
+
+#include
+
+namespace AndroidUtils
+{
+QJniObject getActivity();
+
+QString convertJString(JNIEnv *env, jstring data);
+
+void runOnAndroidThreadSync(const std::function &runnable);
+void runOnAndroidThreadAsync(const std::function &runnable);
+};
+
+#endif // ANDROID_UTILS_H
diff --git a/client/platforms/android/androidutils.cpp b/client/platforms/android/androidutils.cpp
deleted file mode 100644
index 7cc39824..00000000
--- a/client/platforms/android/androidutils.cpp
+++ /dev/null
@@ -1,183 +0,0 @@
-/* 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/. */
-
-#include "androidutils.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "jni.h"
-
-namespace
-{
- AndroidUtils *s_instance = nullptr;
-} // namespace
-
-// static
-QString AndroidUtils::GetDeviceName()
-{
- QJniEnvironment env;
- jclass BUILD = env->FindClass("android/os/Build");
- jfieldID model = env->GetStaticFieldID(BUILD, "MODEL", "Ljava/lang/String;");
- jstring value = (jstring)env->GetStaticObjectField(BUILD, model);
-
- if (!value) {
- return QString("Android Device");
- }
-
- const char *buffer = env->GetStringUTFChars(value, nullptr);
- if (!buffer) {
- return QString("Android Device");
- }
-
- QString res(buffer);
- env->ReleaseStringUTFChars(value, buffer);
-
- return res;
-};
-
-// static
-AndroidUtils *AndroidUtils::instance()
-{
- if (!s_instance) {
- Q_ASSERT(qApp);
- s_instance = new AndroidUtils(qApp);
- }
-
- return s_instance;
-}
-
-AndroidUtils::AndroidUtils(QObject *parent) : QObject(parent)
-{
- Q_ASSERT(!s_instance);
- s_instance = this;
-}
-
-AndroidUtils::~AndroidUtils()
-{
- Q_ASSERT(s_instance == this);
- s_instance = nullptr;
-}
-
-// static
-void AndroidUtils::dispatchToMainThread(std::function callback)
-{
- QTimer *timer = new QTimer();
- timer->moveToThread(qApp->thread());
- timer->setSingleShot(true);
- QObject::connect(timer, &QTimer::timeout, [=]() {
- callback();
- timer->deleteLater();
- });
- QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection);
-}
-
-// static
-QByteArray AndroidUtils::getQByteArrayFromJString(JNIEnv *env, jstring data)
-{
- const char *buffer = env->GetStringUTFChars(data, nullptr);
- if (!buffer) {
- qDebug() << "getQByteArrayFromJString - failed to parse data.";
- return QByteArray();
- }
-
- QByteArray out(buffer);
- env->ReleaseStringUTFChars(data, buffer);
- return out;
-}
-
-// static
-QString AndroidUtils::getQStringFromJString(JNIEnv *env, jstring data)
-{
- const char *buffer = env->GetStringUTFChars(data, nullptr);
- if (!buffer) {
- qDebug() << "getQStringFromJString - failed to parse data.";
- return QString();
- }
-
- QString out(buffer);
- env->ReleaseStringUTFChars(data, buffer);
- return out;
-}
-
-// static
-QJsonObject AndroidUtils::getQJsonObjectFromJString(JNIEnv *env, jstring data)
-{
- QByteArray raw(getQByteArrayFromJString(env, data));
- QJsonParseError jsonError;
- QJsonDocument json = QJsonDocument::fromJson(raw, &jsonError);
- if (QJsonParseError::NoError != jsonError.error) {
- qDebug() << "getQJsonObjectFromJstring - error parsing json. Code: " << jsonError.error
- << "Offset: " << jsonError.offset << "Message: " << jsonError.errorString() << "Data: " << raw;
- return QJsonObject();
- }
-
- if (!json.isObject()) {
- qDebug() << "getQJsonObjectFromJString - object expected.";
- return QJsonObject();
- }
-
- return json.object();
-}
-
-QJniObject AndroidUtils::getActivity()
-{
- return QNativeInterface::QAndroidApplication::context();
-}
-
-int AndroidUtils::GetSDKVersion()
-{
- QJniEnvironment env;
- jclass versionClass = env->FindClass("android/os/Build$VERSION");
- jfieldID sdkIntFieldID = env->GetStaticFieldID(versionClass, "SDK_INT", "I");
- int sdk = env->GetStaticIntField(versionClass, sdkIntFieldID);
-
- return sdk;
-}
-
-QString AndroidUtils::GetManufacturer()
-{
- QJniEnvironment env;
- jclass buildClass = env->FindClass("android/os/Build");
- jfieldID manuFacturerField = env->GetStaticFieldID(buildClass, "MANUFACTURER", "Ljava/lang/String;");
- jstring value = (jstring)env->GetStaticObjectField(buildClass, manuFacturerField);
-
- const char *buffer = env->GetStringUTFChars(value, nullptr);
-
- if (!buffer) {
- qDebug() << "Failed to fetch MANUFACTURER";
- return QByteArray();
- }
-
- QString res(buffer);
- qDebug() << "MANUFACTURER: " << res;
- env->ReleaseStringUTFChars(value, buffer);
- return res;
-}
-
-void AndroidUtils::runOnAndroidThreadSync(const std::function runnable)
-{
- QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable).waitForFinished();
-}
-
-void AndroidUtils::runOnAndroidThreadAsync(const std::function runnable)
-{
- QNativeInterface::QAndroidApplication::runOnAndroidMainThread(runnable);
-}
-
-// Static
-// Creates a copy of the passed QByteArray in the JVM and passes back a ref
-jbyteArray AndroidUtils::tojByteArray(const QByteArray &data)
-{
- QJniEnvironment env;
- jbyteArray out = env->NewByteArray(data.size());
- env->SetByteArrayRegion(out, 0, data.size(), reinterpret_cast(data.constData()));
- return out;
-}
diff --git a/client/platforms/android/androidutils.h b/client/platforms/android/androidutils.h
deleted file mode 100644
index 8559400c..00000000
--- a/client/platforms/android/androidutils.h
+++ /dev/null
@@ -1,49 +0,0 @@
-/* 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/. */
-
-#ifndef ANDROIDUTILS_H
-#define ANDROIDUTILS_H
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-
-class AndroidUtils final : public QObject
-{
- Q_OBJECT
- Q_DISABLE_COPY_MOVE(AndroidUtils)
-
-public:
- static QString GetDeviceName();
-
- static int GetSDKVersion();
- static QString GetManufacturer();
-
- static AndroidUtils* instance();
-
- static void dispatchToMainThread(std::function callback);
-
- static QByteArray getQByteArrayFromJString(JNIEnv* env, jstring data);
-
- static jbyteArray tojByteArray(const QByteArray& data);
-
- static QString getQStringFromJString(JNIEnv* env, jstring data);
-
- static QJsonObject getQJsonObjectFromJString(JNIEnv* env, jstring data);
-
- static QJniObject getActivity();
-
- static void runOnAndroidThreadSync(const std::function runnable);
- static void runOnAndroidThreadAsync(const std::function runnable);
-
-private:
- AndroidUtils(QObject* parent);
- ~AndroidUtils();
-};
-
-#endif // ANDROIDUTILS_H
diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp
index 9209f4cd..4f3fe7d5 100644
--- a/client/ui/controllers/exportController.cpp
+++ b/client/ui/controllers/exportController.cpp
@@ -15,7 +15,7 @@
#include "core/errorstrings.h"
#include "systemController.h"
#ifdef Q_OS_ANDROID
- #include "platforms/android/androidutils.h"
+ #include "platforms/android/android_utils.h"
#endif
#include "qrcodegen.hpp"
diff --git a/client/ui/controllers/pageController.cpp b/client/ui/controllers/pageController.cpp
index ed60500a..105f2115 100644
--- a/client/ui/controllers/pageController.cpp
+++ b/client/ui/controllers/pageController.cpp
@@ -7,7 +7,7 @@
#endif
#ifdef Q_OS_ANDROID
- #include "../../platforms/android/androidutils.h"
+ #include "platforms/android/android_utils.h"
#include
#endif
#if defined Q_OS_MAC
diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp
index 73b9d276..f7345608 100644
--- a/client/ui/controllers/settingsController.cpp
+++ b/client/ui/controllers/settingsController.cpp
@@ -7,8 +7,7 @@
#include "ui/qautostart.h"
#include "version.h"
#ifdef Q_OS_ANDROID
- #include "../../platforms/android/android_controller.h"
- #include "../../platforms/android/androidutils.h"
+ #include "platforms/android/android_utils.h"
#include
#endif
diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp
index 96fc2792..ecd68c8f 100644
--- a/client/ui/controllers/systemController.cpp
+++ b/client/ui/controllers/systemController.cpp
@@ -60,6 +60,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
{
QString fileName;
+#ifdef Q_OS_ANDROID
+ Q_ASSERT(!isSaveMode);
+ return AndroidController::instance()->openFile(nameFilter);
+#endif
+
#ifdef Q_OS_IOS
MobileUtils mobileUtils;
@@ -108,20 +113,6 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
}
fileName = mainFileDialog->property("selectedFile").toString();
-
-#ifdef Q_OS_ANDROID
- // patch for files containing spaces etc
- const QString sep { "raw%3A%2F" };
- if (fileName.startsWith("content://") && fileName.contains(sep)) {
- QString contentUrl = fileName.split(sep).at(0);
- QString rawUrl = fileName.split(sep).at(1);
- rawUrl.replace(" ", "%20");
- fileName = contentUrl + sep + rawUrl;
- }
-
- return fileName;
-#endif
-
return QUrl(fileName).toLocalFile();
}