Refactor Android open file method

Fix some bugs with mimetype filters when the Qt mimetype database does not match the Android database
This commit is contained in:
albexk 2023-12-26 16:23:05 +03:00
parent 7437d47d92
commit e8cc80f046
5 changed files with 89 additions and 14 deletions

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.EXTRA_MIME_TYPES
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 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
@ -12,11 +13,13 @@ import android.os.IBinder
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.os.Messenger import android.os.Messenger
import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.io.IOException import java.io.IOException
import kotlin.LazyThreadSafetyMode.NONE import kotlin.LazyThreadSafetyMode.NONE
import kotlin.text.RegexOption.IGNORE_CASE
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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 CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2 private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val BIND_SERVICE_TIMEOUT = 1000L private const val BIND_SERVICE_TIMEOUT = 1000L
class AmneziaActivity : QtActivity() { 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 -> { CHECK_VPN_PERMISSION_ACTION_CODE -> {
when (resultCode) { when (resultCode) {
RESULT_OK -> { 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") @Suppress("unused")
fun setNotificationText(title: String, message: String, timerSec: Int) { fun setNotificationText(title: String, message: String, timerSec: Int) {
Log.v(TAG, "Set notification text") Log.v(TAG, "Set notification text")

View file

@ -15,6 +15,8 @@ object QtAndroidController {
external fun onVpnReconnecting() external fun onVpnReconnecting()
external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long) external fun onStatisticsUpdate(rxBytes: Long, txBytes: Long)
external fun onFileOpened(uri: String)
external fun onConfigImported(data: String) external fun onConfigImported(data: String)
external fun decodeQrCode(data: String): Boolean external fun decodeQrCode(data: String): Boolean

View file

@ -1,6 +1,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QJniEnvironment> #include <QJniEnvironment>
#include <QJsonDocument> #include <QJsonDocument>
#include <QQmlFile>
#include "android_controller.h" #include "android_controller.h"
#include "ui/controllers/importController.h" #include "ui/controllers/importController.h"
@ -106,6 +107,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)},
{"onFileOpened", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onFileOpened)},
{"onConfigImported", "(Ljava/lang/String;)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)}
}; };
@ -165,6 +167,24 @@ void AndroidController::saveFile(const QString &fileName, const QString &data)
QJniObject::fromString(data).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;
}
void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec) void AndroidController::setNotificationText(const QString &title, const QString &message, int timerSec)
{ {
callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V", callActivityMethod("setNotificationText", "(Ljava/lang/String;Ljava/lang/String;I)V",
@ -284,6 +304,22 @@ void AndroidController::onStatisticsUpdate(JNIEnv *env, jobject thiz, jlong rxBy
emit AndroidController::instance()->statisticsUpdated((quint64) rxBytes, (quint64) txBytes); emit AndroidController::instance()->statisticsUpdated((quint64) rxBytes, (quint64) txBytes);
} }
// static
void AndroidController::onFileOpened(JNIEnv *env, jobject thiz, jstring uri)
{
Q_UNUSED(thiz);
const char *buffer = env->GetStringUTFChars(uri, nullptr);
if (!buffer) {
return;
}
QString lUri(buffer);
env->ReleaseStringUTFChars(uri, buffer);
emit AndroidController::instance()->fileOpened(lUri);
}
// static // static
void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data) void AndroidController::onConfigImported(JNIEnv *env, jobject thiz, jstring data)
{ {

View file

@ -31,6 +31,7 @@ public:
void stop(); void stop();
void setNotificationText(const QString &title, const QString &message, int timerSec); 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(); void startQrReaderActivity();
signals: signals:
@ -43,6 +44,7 @@ signals:
void vpnDisconnected(); void vpnDisconnected();
void vpnReconnecting(); void vpnReconnecting();
void statisticsUpdated(quint64 rxBytes, quint64 txBytes); void statisticsUpdated(quint64 rxBytes, quint64 txBytes);
void fileOpened(QString uri);
void configImported(QString config); void configImported(QString config);
void importConfigFromOutside(QString config); void importConfigFromOutside(QString config);
void initConnectionState(Vpn::ConnectionState state); void initConnectionState(Vpn::ConnectionState state);
@ -65,6 +67,7 @@ private:
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, jstring data); 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); static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
template <typename Ret, typename ...Args> template <typename Ret, typename ...Args>

View file

@ -60,6 +60,11 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix)
{ {
QString fileName; QString fileName;
#ifdef Q_OS_ANDROID
Q_ASSERT(!isSaveMode);
return AndroidController::instance()->openFile(nameFilter);
#endif
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
MobileUtils mobileUtils; MobileUtils mobileUtils;
@ -108,20 +113,6 @@ QString SystemController::getFileName(const QString &acceptLabel, const QString
} }
fileName = mainFileDialog->property("selectedFile").toString(); 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(); return QUrl(fileName).toLocalFile();
} }