amnezia-client/client/platforms/android/android_controller.cpp
albexk 175477d31f
Android qt 6.7 (#1024)
* Up Gradle to 8.10

* Update Android dependencies

* Up Qt to 6.7.2

* Up qtkeychain to 0.14.3

* Move function of changing the color of the navigation bar to the android side

* Fix splashscreen and recent apps thumbnail backgrounds

* Android authentication refactoring

* Fix GitHub action

* Fix the extra circle around the connect button on Android

* Fix keyboard popup

* Increase the amount of requestNetwork attempts on Android 11
2024-09-09 12:36:33 +01:00

501 lines
16 KiB
C++

#include <QJniEnvironment>
#include <QJsonDocument>
#include <QQmlFile>
#include <QEventLoop>
#include <QImage>
#include <android/bitmap.h>
#include "android_controller.h"
#include "android_utils.h"
#include "ui/controllers/importController.h"
namespace
{
AndroidController *s_instance = nullptr;
constexpr auto QT_ANDROID_CONTROLLER_CLASS = "org/amnezia/vpn/qt/QtAndroidController";
constexpr auto ANDROID_LOG_CLASS = "org/amnezia/vpn/util/Log";
constexpr auto TAG = "AmneziaQt";
} // namespace
AndroidController::AndroidController() : QObject()
{
connect(this, &AndroidController::status, this,
[this](AndroidController::ConnectionState state) {
qDebug() << "Android event: status =" << textConnectionState(state);
if (isWaitingStatus) {
qDebug() << "Initialization by service status";
isWaitingStatus = false;
emit initConnectionState(convertState(state));
}
},
Qt::QueuedConnection);
connect(
this, &AndroidController::serviceDisconnected, this,
[this]() {
qDebug() << "Android event: service disconnected";
isWaitingStatus = true;
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
},
Qt::QueuedConnection);
connect(
this, &AndroidController::serviceError, this,
[this]() {
qDebug() << "Android event: service error";
// todo: add error message
emit connectionStateChanged(Vpn::ConnectionState::Error);
},
Qt::QueuedConnection);
connect(
this, &AndroidController::vpnPermissionRejected, this,
[this]() {
qWarning() << "Android event: VPN permission rejected";
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
},
Qt::QueuedConnection);
connect(
this, &AndroidController::vpnStateChanged, this,
[this](AndroidController::ConnectionState state) {
qDebug() << "Android event: VPN state changed:" << textConnectionState(state);
emit connectionStateChanged(convertState(state));
},
Qt::QueuedConnection);
connect(
this, &AndroidController::configImported, this,
[this](const QString& config) {
qDebug() << "Android event: config import";
emit importConfigFromOutside(config);
},
Qt::QueuedConnection);
}
AndroidController *AndroidController::instance()
{
if (!s_instance) {
s_instance = new AndroidController();
}
return s_instance;
}
bool AndroidController::initialize()
{
qDebug() << "Initialize AndroidController";
const JNINativeMethod methods[] = {
{"onStatus", "(I)V", reinterpret_cast<void *>(onStatus)},
{"onServiceDisconnected", "()V", reinterpret_cast<void *>(onServiceDisconnected)},
{"onServiceError", "()V", reinterpret_cast<void *>(onServiceError)},
{"onVpnPermissionRejected", "()V", reinterpret_cast<void *>(onVpnPermissionRejected)},
{"onNotificationStateChanged", "()V", reinterpret_cast<void *>(onNotificationStateChanged)},
{"onVpnStateChanged", "(I)V", reinterpret_cast<void *>(onVpnStateChanged)},
{"onStatisticsUpdate", "(JJ)V", reinterpret_cast<void *>(onStatisticsUpdate)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onConfigImported)},
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)}
};
QJniEnvironment env;
bool registered = env.registerNativeMethods(QT_ANDROID_CONTROLLER_CLASS, methods,
sizeof(methods) / sizeof(JNINativeMethod));
if (!registered) {
qCritical() << "Failed native method registration";
return false;
}
qtAndroidControllerInitialized();
return true;
}
// static
template <typename Ret, typename ...Args>
auto AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args)
{
qDebug() << "Call activity method:" << methodName;
QJniObject activity = AndroidUtils::getActivity();
Q_ASSERT(activity.isValid());
return activity.callMethod<Ret>(methodName, signature, std::forward<Args>(args)...);
}
// static
template <typename ...Args>
void AndroidController::callActivityMethod(const char *methodName, const char *signature, Args &&...args)
{
callActivityMethod<void>(methodName, signature, std::forward<Args>(args)...);
}
ErrorCode AndroidController::start(const QJsonObject &vpnConfig)
{
isWaitingStatus = false;
auto config = QJsonDocument(vpnConfig).toJson();
callActivityMethod("start", "(Ljava/lang/String;)V",
QJniObject::fromString(config).object<jstring>());
return ErrorCode::NoError;
}
void AndroidController::stop()
{
callActivityMethod("stop", "()V");
}
void AndroidController::resetLastServer(int serverIndex)
{
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::saveFile(const QString &fileName, const QString &data)
{
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
QJniObject::fromString(fileName).object<jstring>(),
QJniObject::fromString(data).object<jstring>());
}
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::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
callActivityMethod("openFile", "(Ljava/lang/String;)V",
QJniObject::fromString(filter).object<jstring>());
wait.exec();
return fileName;
}
bool AndroidController::isCameraPresent()
{
return callActivityMethod<jboolean>("isCameraPresent", "()Z");
}
bool AndroidController::isOnTv()
{
return callActivityMethod<jboolean>("isOnTv", "()Z");
}
void AndroidController::startQrReaderActivity()
{
callActivityMethod("startQrCodeReader", "()V");
}
void AndroidController::setSaveLogs(bool enabled)
{
callActivityMethod("setSaveLogs", "(Z)V", enabled);
}
void AndroidController::exportLogsFile(const QString &fileName)
{
callActivityMethod("exportLogsFile", "(Ljava/lang/String;)V",
QJniObject::fromString(fileName).object<jstring>());
}
void AndroidController::clearLogs()
{
callActivityMethod("clearLogs", "()V");
}
void AndroidController::setScreenshotsEnabled(bool enabled)
{
callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled);
}
void AndroidController::setNavigationBarColor(unsigned int color)
{
callActivityMethod("setNavigationBarColor", "(I)V", color);
}
void AndroidController::minimizeApp()
{
callActivityMethod("minimizeApp", "()V");
}
QJsonArray AndroidController::getAppList()
{
QJniObject appList = callActivityMethod<jstring>("getAppList", "()Ljava/lang/String;");
QJsonArray jsonAppList = QJsonDocument::fromJson(appList.toString().toUtf8()).array();
return jsonAppList;
}
QPixmap AndroidController::getAppIcon(const QString &package, QSize *size, const QSize &requestedSize)
{
QJniObject bitmap = callActivityMethod<jobject>("getAppIcon", "(Ljava/lang/String;II)Landroid/graphics/Bitmap;",
QJniObject::fromString(package).object<jstring>(),
requestedSize.width(), requestedSize.height());
QJniEnvironment env;
AndroidBitmapInfo info;
if (AndroidBitmap_getInfo(env.jniEnv(), bitmap.object(), &info) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
void *pixels;
if (AndroidBitmap_lockPixels(env.jniEnv(), bitmap.object(), &pixels) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
int width = info.width;
int height = info.height;
size->setWidth(width);
size->setHeight(height);
QImage image(width, height, QImage::Format_RGBA8888);
if (info.stride == uint32_t(image.bytesPerLine())) {
memcpy((void *) image.constBits(), pixels, info.stride * height);
} else {
auto *bmpPtr = static_cast<uchar *>(pixels);
for (int i = 0; i < height; i++, bmpPtr += info.stride)
memcpy((void *) image.constScanLine(i), bmpPtr, width);
}
if (AndroidBitmap_unlockPixels(env.jniEnv(), bitmap.object()) != ANDROID_BITMAP_RESULT_SUCCESS) return {};
return QPixmap::fromImage(image);
}
bool AndroidController::isNotificationPermissionGranted()
{
return callActivityMethod<jboolean>("isNotificationPermissionGranted", "()Z");
}
void AndroidController::requestNotificationPermission()
{
callActivityMethod("requestNotificationPermission", "()V");
}
bool AndroidController::requestAuthentication()
{
QEventLoop wait;
bool result;
connect(this, &AndroidController::authenticationResult, this,
[&result, &wait](const bool &authResult){
qDebug() << "Android authentication result:" << authResult;
result = authResult;
wait.quit();
},
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::SingleShotConnection));
callActivityMethod("requestAuthentication", "()V");
wait.exec();
return result;
}
// Moving log processing to the Android side
jclass AndroidController::log;
jmethodID AndroidController::logDebug;
jmethodID AndroidController::logInfo;
jmethodID AndroidController::logWarning;
jmethodID AndroidController::logError;
jmethodID AndroidController::logFatal;
// static
bool AndroidController::initLogging()
{
QJniEnvironment env;
log = env.findClass(ANDROID_LOG_CLASS);
if (log == nullptr) {
qCritical() << "Android log class" << ANDROID_LOG_CLASS << "not found";
return false;
}
auto logMethodSignature = "(Ljava/lang/String;Ljava/lang/String;)V";
logDebug = env.findStaticMethod(log, "d", logMethodSignature);
if (logDebug == nullptr) {
qCritical() << "Android debug log method not found";
return false;
}
logInfo = env.findStaticMethod(log, "i", logMethodSignature);
if (logInfo == nullptr) {
qCritical() << "Android info log method not found";
return false;
}
logWarning = env.findStaticMethod(log, "w", logMethodSignature);
if (logWarning == nullptr) {
qCritical() << "Android warning log method not found";
return false;
}
logError = env.findStaticMethod(log, "e", logMethodSignature);
if (logError == nullptr) {
qCritical() << "Android error log method not found";
return false;
}
logFatal = env.findStaticMethod(log, "f", logMethodSignature);
if (logFatal == nullptr) {
qCritical() << "Android fatal log method not found";
return false;
}
qInstallMessageHandler(messageHandler);
return true;
}
// static
void AndroidController::messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
{
jmethodID logMethod = logDebug;
switch (type) {
case QtDebugMsg:
logMethod = logDebug;
break;
case QtInfoMsg:
logMethod = logInfo;
break;
case QtWarningMsg:
logMethod = logWarning;
break;
case QtCriticalMsg:
logMethod = logError;
break;
case QtFatalMsg:
logMethod = logFatal;
break;
}
QString formattedMessage = qFormatLogMessage(type, context, message);
QJniObject::callStaticMethod<void>(log, logMethod,
QJniObject::fromString(TAG).object<jstring>(),
QJniObject::fromString(formattedMessage).object<jstring>());
}
void AndroidController::qtAndroidControllerInitialized()
{
callActivityMethod("qtAndroidControllerInitialized", "()V");
}
// static
Vpn::ConnectionState AndroidController::convertState(AndroidController::ConnectionState state)
{
switch (state) {
case AndroidController::ConnectionState::CONNECTED: return Vpn::ConnectionState::Connected;
case AndroidController::ConnectionState::CONNECTING: return Vpn::ConnectionState::Connecting;
case AndroidController::ConnectionState::DISCONNECTED: return Vpn::ConnectionState::Disconnected;
case AndroidController::ConnectionState::DISCONNECTING: return Vpn::ConnectionState::Disconnecting;
case AndroidController::ConnectionState::RECONNECTING: return Vpn::ConnectionState::Reconnecting;
case AndroidController::ConnectionState::UNKNOWN: return Vpn::ConnectionState::Unknown;
}
}
// static
QString AndroidController::textConnectionState(AndroidController::ConnectionState state)
{
switch (state) {
case AndroidController::ConnectionState::CONNECTED: return "CONNECTED";
case AndroidController::ConnectionState::CONNECTING: return "CONNECTING";
case AndroidController::ConnectionState::DISCONNECTED: return "DISCONNECTED";
case AndroidController::ConnectionState::DISCONNECTING: return "DISCONNECTING";
case AndroidController::ConnectionState::RECONNECTING: return "RECONNECTING";
case AndroidController::ConnectionState::UNKNOWN: return "UNKNOWN";
}
}
// JNI functions called by Android
// static
void AndroidController::onStatus(JNIEnv *env, jobject thiz, jint stateCode)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
auto state = ConnectionState(stateCode);
emit AndroidController::instance()->status(state);
}
// static
void AndroidController::onServiceDisconnected(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->serviceDisconnected();
}
// static
void AndroidController::onServiceError(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->serviceError();
}
// static
void AndroidController::onVpnPermissionRejected(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->vpnPermissionRejected();
}
// static
void AndroidController::onNotificationStateChanged(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->notificationStateChanged();
}
// static
void AndroidController::onVpnStateChanged(JNIEnv *env, jobject thiz, jint stateCode)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
auto state = ConnectionState(stateCode);
emit AndroidController::instance()->vpnStateChanged(state);
}
// static
void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBytes, jlong txBytes)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->statisticsUpdated((quint64) rxBytes, (quint64) txBytes);
}
// static
void AndroidController::onFileOpened(JNIEnv *env, jobject thiz, jstring uri)
{
Q_UNUSED(thiz);
emit AndroidController::instance()->fileOpened(AndroidUtils::convertJString(env, uri));
}
// static
void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
{
Q_UNUSED(thiz);
emit AndroidController::instance()->configImported(AndroidUtils::convertJString(env, data));
}
// static
void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result)
{
Q_UNUSED(thiz);
emit AndroidController::instance()->authenticationResult(result);
}
// static
bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{
Q_UNUSED(thiz);
return ImportController::decodeQrCode(AndroidUtils::convertJString(env, data));
}