diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index dd1148a39..29a6d06ba 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -13,6 +13,7 @@ import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.ServiceConnection import android.content.pm.PackageManager import android.graphics.Bitmap +import android.net.Uri import android.net.VpnService import android.os.Build import android.os.Bundle @@ -21,6 +22,8 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.os.ParcelFileDescriptor +import android.provider.OpenableColumns import android.provider.Settings import android.view.MotionEvent import android.view.WindowManager.LayoutParams @@ -29,6 +32,7 @@ import android.widget.Toast import androidx.annotation.MainThread import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat +import java.io.FileNotFoundException import java.io.IOException import kotlin.LazyThreadSafetyMode.NONE import kotlin.text.RegexOption.IGNORE_CASE @@ -72,6 +76,7 @@ class AmneziaActivity : QtActivity() { private var isInBoundState = false private var notificationStateReceiver: BroadcastReceiver? = null private lateinit var vpnServiceMessenger: IpcMessenger + private var pfd: ParcelFileDescriptor? = null private val actionResultHandlers = mutableMapOf() private val permissionRequestHandlers = mutableMapOf() @@ -564,6 +569,11 @@ class AmneziaActivity : QtActivity() { } } }.also { + if (packageManager.resolveActivity(it, PackageManager.MATCH_DEFAULT_ONLY) == null) { + Log.w(TAG, "Not found activity for ACTION_OPEN_DOCUMENT intent") + it.action = Intent.ACTION_GET_CONTENT + } + try { startActivityForResult(it, OPEN_FILE_ACTION_CODE, ActivityResultHandler( onAny = { @@ -582,6 +592,33 @@ class AmneziaActivity : QtActivity() { } } + @Suppress("unused") + fun getFd(fileName: String): Int = try { + Log.v(TAG, "Get fd for $fileName") + pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r") + pfd?.fd ?: -1 + } catch (e: FileNotFoundException) { + Log.e(TAG, "Failed to get fd: $e") + -1 + } + + @Suppress("unused") + fun closeFd() { + Log.v(TAG, "Close fd") + pfd?.close() + pfd = null + } + + @Suppress("unused") + fun getFileName(uri: String): String { + contentResolver.query(Uri.parse(uri), arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor -> + if (cursor.moveToFirst() && !cursor.isNull(0)) { + return cursor.getString(0) + } + } + return "" + } + @Suppress("unused") @SuppressLint("UnsupportedChromeOsCameraSystemFeature") fun isCameraPresent(): Boolean = applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA) diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index 5776cc34e..6ea8bea58 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -163,11 +163,7 @@ QString AndroidController::openFile(const QString &filter) QString fileName; connect(this, &AndroidController::fileOpened, this, [&fileName, &wait](const QString &uri) { - qDebug() << "Android event: file opened; uri:" << uri; - fileName = QQmlFile::urlToLocalFileOrQrc(uri); - qDebug() << "Qt url to local file:" << fileName; - // if qt failed, try using just uri - if (fileName.isEmpty()) fileName = uri; + fileName = uri; wait.quit(); }, static_cast(Qt::QueuedConnection | Qt::SingleShotConnection)); @@ -177,6 +173,25 @@ QString AndroidController::openFile(const QString &filter) return fileName; } +int AndroidController::getFd(const QString &fileName) +{ + return callActivityMethod("getFd", "(Ljava/lang/String;)I", + QJniObject::fromString(fileName).object()); +} + +void AndroidController::closeFd() +{ + callActivityMethod("closeFd", "()V"); +} + +QString AndroidController::getFileName(const QString &uri) +{ + auto fileName = callActivityMethod("getFileName", "(Ljava/lang/String;)Ljava/lang/String;", + QJniObject::fromString(uri).object()); + QJniEnvironment env; + return AndroidUtils::convertJString(env.jniEnv(), fileName.object()); +} + bool AndroidController::isCameraPresent() { return callActivityMethod("isCameraPresent", "()Z"); diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 759c9c3f2..c2e082eab 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -34,6 +34,9 @@ class AndroidController : public QObject void resetLastServer(int serverIndex); void saveFile(const QString &fileName, const QString &data); QString openFile(const QString &filter); + int getFd(const QString &fileName); + void closeFd(); + QString getFileName(const QString &uri); bool isCameraPresent(); bool isOnTv(); void startQrReaderActivity(); diff --git a/client/ui/controllers/importController.cpp b/client/ui/controllers/importController.cpp index f7e96bfff..3200d0e82 100644 --- a/client/ui/controllers/importController.cpp +++ b/client/ui/controllers/importController.cpp @@ -9,6 +9,7 @@ #include "core/errorstrings.h" #include "core/serialization/serialization.h" +#include "systemController.h" #include "utilities.h" #ifdef Q_OS_ANDROID @@ -76,17 +77,18 @@ ImportController::ImportController(const QSharedPointer &serversMo bool ImportController::extractConfigFromFile(const QString &fileName) { - QFile file(fileName); - - if (file.open(QIODevice::ReadOnly)) { - QString data = file.readAll(); - - m_configFileName = QFileInfo(file.fileName()).fileName(); - return extractConfigFromData(data); + QString data; + if (!SystemController::readFile(fileName, &data)) { + emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); + return false; } - - emit importErrorOccurred(ErrorCode::ImportOpenConfigError, false); - return false; + m_configFileName = QFileInfo(QFile(fileName).fileName()).fileName(); +#ifdef Q_OS_ANDROID + if (m_configFileName.isEmpty()) { + m_configFileName = AndroidController::instance()->getFileName(fileName); + } +#endif + return extractConfigFromData(data); } bool ImportController::extractConfigFromData(QString data) diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index c3945512f..e20f4d3b9 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -131,12 +131,8 @@ void SettingsController::backupAppConfig(const QString &fileName) void SettingsController::restoreAppConfig(const QString &fileName) { - QFile file(fileName); - - file.open(QIODevice::ReadOnly); - - QByteArray data = file.readAll(); - + QByteArray data; + SystemController::readFile(fileName, &data); restoreAppConfigFromData(data); } diff --git a/client/ui/controllers/sitesController.cpp b/client/ui/controllers/sitesController.cpp index d54dbdd2f..d94a9b6f9 100644 --- a/client/ui/controllers/sitesController.cpp +++ b/client/ui/controllers/sitesController.cpp @@ -82,14 +82,12 @@ void SitesController::removeSite(int index) void SitesController::importSites(const QString &fileName, bool replaceExisting) { - QFile file(fileName); - - if (!file.open(QIODevice::ReadOnly)) { + QByteArray jsonData; + if (!SystemController::readFile(fileName, &jsonData)) { emit errorOccurred(tr("Can't open file: %1").arg(fileName)); return; } - QByteArray jsonData = file.readAll(); QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonData); if (jsonDocument.isNull()) { emit errorOccurred(tr("Failed to parse JSON data from file: %1").arg(fileName)); diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 4598bff16..6de667691 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -24,7 +24,7 @@ SystemController::SystemController(const std::shared_ptr &settings, QO { } -void SystemController::saveFile(QString fileName, const QString &data) +void SystemController::saveFile(const QString &fileName, const QString &data) { #if defined Q_OS_ANDROID AndroidController::instance()->saveFile(fileName, data); @@ -62,6 +62,31 @@ void SystemController::saveFile(QString fileName, const QString &data) #endif } +bool SystemController::readFile(const QString &fileName, QByteArray *data) +{ +#ifdef Q_OS_ANDROID + int fd = AndroidController::instance()->getFd(fileName); + if (fd == -1) return false; + QFile file; + if(!file.open(fd, QIODevice::ReadOnly)) return false; + data->assign(file.readAll()); + AndroidController::instance()->closeFd(); +#else + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) return false; + data->assign(file.readAll()); +#endif + return true; +} + +bool SystemController::readFile(const QString &fileName, QString *data) +{ + QByteArray byteArray; + if(!readFile(fileName, &byteArray)) return false; + data->assign(byteArray); + return true; +} + QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index d2ee6f637..ddc3476a3 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -11,7 +11,9 @@ class SystemController : public QObject public: explicit SystemController(const std::shared_ptr &setting, QObject *parent = nullptr); - static void saveFile(QString fileName, const QString &data); + static void saveFile(const QString &fileName, const QString &data); + static bool readFile(const QString &fileName, QByteArray *data); + static bool readFile(const QString &fileName, QString *data); public slots: QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",