Show native file dialog under Android
This commit is contained in:
parent
fc1f6e7b91
commit
6d786d0b77
|
@ -71,6 +71,14 @@ set(QML_SRC_FILES
|
||||||
resources/icons.qrc
|
resources/icons.qrc
|
||||||
resources/qml.qrc
|
resources/qml.qrc
|
||||||
)
|
)
|
||||||
|
if(ANDROID)
|
||||||
|
list(APPEND QML_HEADER_FILES
|
||||||
|
quickgui/android.h
|
||||||
|
)
|
||||||
|
list(APPEND QML_SRC_FILES
|
||||||
|
quickgui/android.cpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(TS_FILES
|
set(TS_FILES
|
||||||
translations/${META_PROJECT_NAME}_de_DE.ts
|
translations/${META_PROJECT_NAME}_de_DE.ts
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<application android:icon="@drawable/icon" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="@string/app_name">
|
<application android:icon="@drawable/icon" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="@string/app_name">
|
||||||
<activity
|
<activity
|
||||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
|
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
|
||||||
android:name="org.qtproject.qt5.android.bindings.QtActivity"
|
android:name="org.martchus.passwordmanager.Activity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:screenOrientation="unspecified"
|
android:screenOrientation="unspecified"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<!-- Splash screen -->
|
<!-- Splash screen -->
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21"/>
|
<uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23"/>
|
||||||
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
|
||||||
|
|
||||||
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:2.2.3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://maven.google.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
compile 'com.android.support:support-v4:27.1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
/*******************************************************
|
||||||
|
* The following variables:
|
||||||
|
* - androidBuildToolsVersion,
|
||||||
|
* - androidCompileSdkVersion
|
||||||
|
* - qt5AndroidDir - holds the path to qt android files
|
||||||
|
* needed to build any Qt application
|
||||||
|
* on Android.
|
||||||
|
*
|
||||||
|
* are defined in gradle.properties file. This file is
|
||||||
|
* updated by QtCreator and androiddeployqt tools.
|
||||||
|
* Changing them manually might break the compilation!
|
||||||
|
*******************************************************/
|
||||||
|
|
||||||
|
compileSdkVersion androidCompileSdkVersion.toInteger()
|
||||||
|
|
||||||
|
buildToolsVersion androidBuildToolsVersion
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
manifest.srcFile 'AndroidManifest.xml'
|
||||||
|
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
|
||||||
|
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
|
||||||
|
res.srcDirs = [qt5AndroidDir + '/res', 'res']
|
||||||
|
resources.srcDirs = ['src']
|
||||||
|
renderscript.srcDirs = ['src']
|
||||||
|
assets.srcDirs = ['assets']
|
||||||
|
jniLibs.srcDirs = ['libs']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package org.martchus.passwordmanager;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.support.v4.provider.DocumentFile;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import org.qtproject.qt5.android.bindings.QtActivity;
|
||||||
|
|
||||||
|
public class Activity extends QtActivity {
|
||||||
|
private final int REQUEST_CODE_PICK_DIR = 1;
|
||||||
|
private final int REQUEST_CODE_PICK_EXISTING_FILE = 2;
|
||||||
|
private final int REQUEST_CODE_PICK_NEW_FILE = 3;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Shows the native Android file dialog. Results are handled in onActivityResult().
|
||||||
|
*/
|
||||||
|
public boolean showAndroidFileDialog(boolean existing) {
|
||||||
|
String action = existing ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT;
|
||||||
|
int requestCode = existing ? REQUEST_CODE_PICK_EXISTING_FILE : REQUEST_CODE_PICK_NEW_FILE;
|
||||||
|
Intent intent = new Intent(action);
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
try {
|
||||||
|
startActivityForResult(intent, requestCode);
|
||||||
|
return true;
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Opens a native file descriptor for the specified \a contentUri (content://...) with the specified \a mode.
|
||||||
|
* \returns Returns the file descriptor or -1 on error.
|
||||||
|
* \remarks \a mode must be "r" and/or "w" for read and/or write. Appending "t" for truncate is possible as well.
|
||||||
|
*/
|
||||||
|
public int openFileDescriptorFromAndroidContentUri(String contentUri, String mode) {
|
||||||
|
try {
|
||||||
|
DocumentFile file = DocumentFile.fromSingleUri(this, Uri.parse(contentUri));
|
||||||
|
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(file.getUri(), mode);
|
||||||
|
return fd.detachFd();
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
switch (requestCode) {
|
||||||
|
case REQUEST_CODE_PICK_EXISTING_FILE:
|
||||||
|
case REQUEST_CODE_PICK_NEW_FILE:
|
||||||
|
boolean existingFile = requestCode == REQUEST_CODE_PICK_EXISTING_FILE;
|
||||||
|
|
||||||
|
if (resultCode != RESULT_OK) {
|
||||||
|
onAndroidFileDialogRejected();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
try {
|
||||||
|
DocumentFile file = DocumentFile.fromSingleUri(this, uri);
|
||||||
|
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(file.getUri(), existingFile ? "r" : "wt");
|
||||||
|
onAndroidFileDialogAcceptedDescriptor(file.getUri().toString(), file.getName(), fd.detachFd(), existingFile);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
onAndroidError("Failed to find selected file.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileName = data.getDataString();
|
||||||
|
if (fileName != null) {
|
||||||
|
onAndroidFileDialogAccepted(fileName, existingFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onAndroidError("Failed to read result from Android's file dialog.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static native void onAndroidError(String message);
|
||||||
|
public static native void onAndroidFileDialogAccepted(String fileName, boolean existing);
|
||||||
|
public static native void onAndroidFileDialogAcceptedDescriptor(String nativeUrl, String fileName, int fileDescriptor, boolean existing);
|
||||||
|
public static native void onAndroidFileDialogRejected();
|
||||||
|
}
|
43
qml/main.qml
43
qml/main.qml
|
@ -71,6 +71,12 @@ Kirigami.ApplicationWindow {
|
||||||
onTriggered: nativeInterface.close()
|
onTriggered: nativeInterface.close()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Controls.Switch {
|
||||||
|
text: qsTr("Use native file dialog")
|
||||||
|
checked: nativeInterface.useNativeFileDialog
|
||||||
|
visible: nativeInterface.supportsNativeFileDialog
|
||||||
|
onCheckedChanged: nativeInterface.useNativeFileDialog = checked
|
||||||
|
}
|
||||||
Controls.Switch {
|
Controls.Switch {
|
||||||
id: showPasswordsOnFocusSwitch
|
id: showPasswordsOnFocusSwitch
|
||||||
text: qsTr("Show passwords on focus")
|
text: qsTr("Show passwords on focus")
|
||||||
|
@ -97,27 +103,31 @@ Kirigami.ApplicationWindow {
|
||||||
id: fileDialog
|
id: fileDialog
|
||||||
title: selectExisting ? qsTr("Select an existing file") : qsTr(
|
title: selectExisting ? qsTr("Select an existing file") : qsTr(
|
||||||
"Select path for new file")
|
"Select path for new file")
|
||||||
|
property bool selectExisting: true
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (fileUrls.length < 1) {
|
if (fileUrls.length < 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (selectExisting) {
|
nativeInterface.handleFileSelectionAccepted(fileUrls[0],
|
||||||
nativeInterface.load(fileUrls[0])
|
this.selectExisting)
|
||||||
} else {
|
|
||||||
nativeInterface.create(fileUrls[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onRejected: {
|
|
||||||
showPassiveNotification(qsTr("Canceled file selection"))
|
|
||||||
}
|
}
|
||||||
|
onRejected: nativeInterface.handleFileSelectionCanceled()
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
if (nativeInterface.showNativeFileDialog(this.selectExisting)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// fallback to the Qt Quick file dialog if a native implementation is not available
|
||||||
|
this.open()
|
||||||
|
}
|
||||||
function openExisting() {
|
function openExisting() {
|
||||||
this.selectExisting = true
|
this.selectExisting = true
|
||||||
this.open()
|
this.show()
|
||||||
}
|
}
|
||||||
function createNew() {
|
function createNew() {
|
||||||
this.selectExisting = false
|
this.selectExisting = false
|
||||||
this.open()
|
this.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,6 +159,9 @@ Kirigami.ApplicationWindow {
|
||||||
showPassiveNotification(qsTr("%1 saved").arg(
|
showPassiveNotification(qsTr("%1 saved").arg(
|
||||||
nativeInterface.fileName))
|
nativeInterface.fileName))
|
||||||
}
|
}
|
||||||
|
onNewNotification: {
|
||||||
|
showPassiveNotification(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
|
@ -173,17 +186,17 @@ Kirigami.ApplicationWindow {
|
||||||
|
|
||||||
function pushStackEntry(entryModel, rootIndex) {
|
function pushStackEntry(entryModel, rootIndex) {
|
||||||
pageStack.push(entriesComponent.createObject(root, {
|
pageStack.push(entriesComponent.createObject(root, {
|
||||||
entryModel: entryModel,
|
"entryModel": entryModel,
|
||||||
rootIndex: rootIndex,
|
"rootIndex": rootIndex,
|
||||||
title: entryModel.data(
|
"title": entryModel.data(
|
||||||
rootIndex)
|
rootIndex)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFileActions(files) {
|
function createFileActions(files) {
|
||||||
return files.map(function (filePath) {
|
return files.map(function (filePath) {
|
||||||
return this.createObject(root, {
|
return this.createObject(root, {
|
||||||
filePath: filePath
|
"filePath": filePath
|
||||||
})
|
})
|
||||||
}, fileActionComponent)
|
}, fileActionComponent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
#include "./android.h"
|
||||||
|
#include "./controller.h"
|
||||||
|
|
||||||
|
#include "resources/config.h"
|
||||||
|
|
||||||
|
#include <c++utilities/conversion/stringbuilder.h>
|
||||||
|
|
||||||
|
#include <QtAndroid>
|
||||||
|
#include <QAndroidJniObject>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QMetaObject>
|
||||||
|
#include <QMessageLogContext>
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
using namespace ConversionUtilities;
|
||||||
|
|
||||||
|
namespace QtGui {
|
||||||
|
|
||||||
|
static Controller *controllerForAndroid = nullptr;
|
||||||
|
|
||||||
|
void registerControllerForAndroid(Controller *controller)
|
||||||
|
{
|
||||||
|
controllerForAndroid = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool showAndroidFileDialog(bool existing)
|
||||||
|
{
|
||||||
|
return QtAndroid::androidActivity().callMethod<jboolean>("showAndroidFileDialog", "(Z)Z", existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode)
|
||||||
|
{
|
||||||
|
return QtAndroid::androidActivity().callMethod<jint>("openFileDescriptorFromAndroidContentUri", "(Ljava/lang/String;Ljava/lang/String;)I", QAndroidJniObject::fromString(url).object<jstring>(), QAndroidJniObject::fromString(mode).object<jstring>());
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
|
||||||
|
constexpr auto tag = PROJECT_NAME "-" APP_VERSION;
|
||||||
|
auto report = msg.toStdString();
|
||||||
|
if (context.file && *context.file) {
|
||||||
|
report += argsToString(" in file ", context.file, " line ", context.line);
|
||||||
|
}
|
||||||
|
if (context.function && !QString(context.function).isEmpty()) {
|
||||||
|
report += argsToString(" function ", context.function);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case QtDebugMsg:
|
||||||
|
__android_log_write(ANDROID_LOG_DEBUG, tag, report.data());
|
||||||
|
break;
|
||||||
|
case QtInfoMsg:
|
||||||
|
__android_log_write(ANDROID_LOG_INFO,tag,report.data());
|
||||||
|
break;
|
||||||
|
case QtWarningMsg:
|
||||||
|
__android_log_write(ANDROID_LOG_WARN,tag,report.data());
|
||||||
|
break;
|
||||||
|
case QtCriticalMsg:
|
||||||
|
__android_log_write(ANDROID_LOG_ERROR,tag,report.data());
|
||||||
|
break;
|
||||||
|
case QtFatalMsg:
|
||||||
|
__android_log_write(ANDROID_LOG_FATAL,tag,report.data());
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onAndroidError(JNIEnv *, jobject, jstring message)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "newNotification", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(message).toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onAndroidFileDialogAccepted(JNIEnv *, jobject, jstring fileName, jboolean existing)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAccepted", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()), Q_ARG(bool, existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onAndroidFileDialogAcceptedDescriptor(JNIEnv *, jobject, jstring nativeUrl, jstring fileName, jint fileHandle, jboolean existing)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAcceptedDescriptor", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject::fromLocalRef(nativeUrl).toString()), Q_ARG(QString, QAndroidJniObject::fromLocalRef(fileName).toString()), Q_ARG(int, fileHandle), Q_ARG(bool, existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onAndroidFileDialogRejected(JNIEnv *, jobject)
|
||||||
|
{
|
||||||
|
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionCanceled", Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Registers the static functions declared above so they can be called from the Java-side.
|
||||||
|
* \remarks This method is called automatically by Java after the .so file is loaded.
|
||||||
|
*/
|
||||||
|
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *)
|
||||||
|
{
|
||||||
|
// get the JNIEnv pointer
|
||||||
|
JNIEnv *env;
|
||||||
|
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
|
||||||
|
return JNI_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for Java class which declares the native methods
|
||||||
|
const auto javaClass = env->FindClass("org/martchus/passwordmanager/Activity");
|
||||||
|
if (!javaClass) {
|
||||||
|
return JNI_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// register native methods
|
||||||
|
static const JNINativeMethod methods[] = {
|
||||||
|
{"onAndroidError", "(Ljava/lang/String;)V", reinterpret_cast<void *>(onAndroidError)},
|
||||||
|
{"onAndroidFileDialogAccepted", "(Ljava/lang/String;Z)V", reinterpret_cast<void *>(onAndroidFileDialogAccepted)},
|
||||||
|
{"onAndroidFileDialogAcceptedDescriptor", "(Ljava/lang/String;Ljava/lang/String;IZ)V", reinterpret_cast<void *>(onAndroidFileDialogAcceptedDescriptor)},
|
||||||
|
{"onAndroidFileDialogRejected", "()V", reinterpret_cast<void *>(onAndroidFileDialogRejected)},
|
||||||
|
};
|
||||||
|
if (env->RegisterNatives(javaClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
|
||||||
|
return JNI_ERR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JNI_VERSION_1_6;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
#ifndef QT_QUICK_GUI_ANDROID_H
|
||||||
|
#define QT_QUICK_GUI_ANDROID_H
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QMessageLogContext;
|
||||||
|
class QString;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
namespace QtGui {
|
||||||
|
|
||||||
|
class Controller;
|
||||||
|
void registerControllerForAndroid(Controller *controller);
|
||||||
|
bool showAndroidFileDialog(bool existing);
|
||||||
|
int openFileDescriptorFromAndroidContentUrl(const QString &url, const QString &mode);
|
||||||
|
void writeToAndroidLog(QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // QT_QUICK_GUI_ANDROID_H
|
|
@ -1,4 +1,5 @@
|
||||||
#include "./controller.h"
|
#include "./controller.h"
|
||||||
|
#include "./android.h"
|
||||||
|
|
||||||
#include <passwordfile/io/cryptoexception.h>
|
#include <passwordfile/io/cryptoexception.h>
|
||||||
#include <passwordfile/io/parsingexception.h>
|
#include <passwordfile/io/parsingexception.h>
|
||||||
|
@ -6,6 +7,7 @@
|
||||||
#include <qtutilities/misc/dialogutils.h>
|
#include <qtutilities/misc/dialogutils.h>
|
||||||
|
|
||||||
#include <c++utilities/io/catchiofailure.h>
|
#include <c++utilities/io/catchiofailure.h>
|
||||||
|
#include <c++utilities/io/nativefilestream.h>
|
||||||
#include <c++utilities/io/path.h>
|
#include <c++utilities/io/path.h>
|
||||||
|
|
||||||
#ifndef QT_NO_CLIPBOARD
|
#ifndef QT_NO_CLIPBOARD
|
||||||
|
@ -18,6 +20,8 @@
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QStringBuilder>
|
#include <QStringBuilder>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
@ -32,12 +36,14 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
|
||||||
, m_settings(settings)
|
, m_settings(settings)
|
||||||
, m_fileOpen(false)
|
, m_fileOpen(false)
|
||||||
, m_fileModified(false)
|
, m_fileModified(false)
|
||||||
|
, m_useNativeFileDialog(false)
|
||||||
{
|
{
|
||||||
m_entryFilterModel.setSourceModel(&m_entryModel);
|
m_entryFilterModel.setSourceModel(&m_entryModel);
|
||||||
|
|
||||||
// share settings with main window
|
// share settings with main window
|
||||||
m_settings.beginGroup(QStringLiteral("mainwindow"));
|
m_settings.beginGroup(QStringLiteral("mainwindow"));
|
||||||
m_recentFiles = m_settings.value(QStringLiteral("recententries")).toStringList();
|
m_recentFiles = m_settings.value(QStringLiteral("recententries")).toStringList();
|
||||||
|
m_useNativeFileDialog = m_settings.value(QStringLiteral("usenativefiledialog"), m_useNativeFileDialog).toBool();
|
||||||
|
|
||||||
// set initial file path
|
// set initial file path
|
||||||
setFilePath(filePath);
|
setFilePath(filePath);
|
||||||
|
@ -66,7 +72,7 @@ void Controller::setFilePath(const QString &filePath)
|
||||||
emit filePathChanged(m_filePath = filePath);
|
emit filePathChanged(m_filePath = filePath);
|
||||||
|
|
||||||
// handle recent files
|
// handle recent files
|
||||||
auto index = m_recentFiles.indexOf(m_filePath);
|
const auto index = m_recentFiles.indexOf(m_filePath);
|
||||||
if (!index) {
|
if (!index) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -131,13 +137,18 @@ void Controller::create(const QString &filePath)
|
||||||
|
|
||||||
resetFileStatus();
|
resetFileStatus();
|
||||||
try {
|
try {
|
||||||
|
if (filePath.isEmpty()) {
|
||||||
|
m_file.clear();
|
||||||
|
}
|
||||||
m_file.create();
|
m_file.create();
|
||||||
m_entryModel.setRootEntry(m_file.rootEntry());
|
|
||||||
setFileOpen(true);
|
|
||||||
updateWindowTitle();
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
emitIoError(tr("creating"));
|
emitIoError(tr("creating"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_file.generateRootEntry();
|
||||||
|
m_entryModel.setRootEntry(m_file.rootEntry());
|
||||||
|
setFileOpen(true);
|
||||||
|
updateWindowTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::close()
|
void Controller::close()
|
||||||
|
@ -153,13 +164,39 @@ void Controller::close()
|
||||||
void Controller::save()
|
void Controller::save()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
if (!m_password.isEmpty()) {
|
const auto useEncryption = !m_password.isEmpty();
|
||||||
|
if (useEncryption) {
|
||||||
const auto passwordUtf8(m_password.toUtf8());
|
const auto passwordUtf8(m_password.toUtf8());
|
||||||
m_file.setPassword(string(passwordUtf8.data(), static_cast<size_t>(passwordUtf8.size())));
|
m_file.setPassword(string(passwordUtf8.data(), static_cast<size_t>(passwordUtf8.size())));
|
||||||
} else {
|
} else {
|
||||||
m_file.clearPassword();
|
m_file.clearPassword();
|
||||||
}
|
}
|
||||||
m_file.save(!m_password.isEmpty());
|
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
if (!m_nativeUrl.isEmpty()) {
|
||||||
|
// ensure file is closed
|
||||||
|
m_file.close();
|
||||||
|
|
||||||
|
// open new file descriptor to replace existing file and allow writing
|
||||||
|
qDebug() << "opening new fd for saving, native url: " << m_nativeUrl;
|
||||||
|
const auto newFileDescriptor = openFileDescriptorFromAndroidContentUrl(m_nativeUrl, QStringLiteral("wt"));
|
||||||
|
if (newFileDescriptor < 0) {
|
||||||
|
emit fileError(tr("Unable to open file descriptor for saving the file."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_file.fileStream().openFromFileDescriptor(newFileDescriptor, ios_base::out | ios_base::trunc | ios_base::binary);
|
||||||
|
qDebug() << "file re-opened for saving";
|
||||||
|
|
||||||
|
m_file.write(useEncryption);
|
||||||
|
} else {
|
||||||
|
#endif
|
||||||
|
// let libpasswordfile handle everything
|
||||||
|
qDebug() << "let libpasswordfile handle saving";
|
||||||
|
m_file.save(useEncryption);
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
emit fileSaved();
|
emit fileSaved();
|
||||||
} catch (const CryptoException &e) {
|
} catch (const CryptoException &e) {
|
||||||
emit fileError(tr("A crypto error occured when saving the file: ") + QString::fromLocal8Bit(e.what()));
|
emit fileError(tr("A crypto error occured when saving the file: ") + QString::fromLocal8Bit(e.what()));
|
||||||
|
@ -170,12 +207,59 @@ void Controller::save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Shows a native file dialog if supported; otherwise returns false.
|
||||||
|
* \remarks If supported, this method will load/create the selected file (according to \a existing).
|
||||||
|
*/
|
||||||
|
bool Controller::showNativeFileDialog(bool existing)
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
if (!m_useNativeFileDialog) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return showAndroidFileDialog(existing);
|
||||||
|
#else
|
||||||
|
Q_UNUSED(existing)
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::handleFileSelectionAccepted(const QString &filePath, bool existing)
|
||||||
|
{
|
||||||
|
m_nativeUrl.clear();
|
||||||
|
if (existing) {
|
||||||
|
load(filePath);
|
||||||
|
} else {
|
||||||
|
create(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
void Controller::handleFileSelectionAcceptedDescriptor(const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
m_file.setPath(fileName.toStdString());
|
||||||
|
m_file.fileStream().openFromFileDescriptor(fileDescriptor, ios_base::in | ios_base::binary);
|
||||||
|
m_file.opened();
|
||||||
|
} catch (...) {
|
||||||
|
emitIoError(tr("opening from native file descriptor"));
|
||||||
|
}
|
||||||
|
emit filePathChanged(m_filePath = m_fileName = fileName);
|
||||||
|
handleFileSelectionAccepted(QString(), existing);
|
||||||
|
m_nativeUrl = nativeUrl;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Controller::handleFileSelectionCanceled()
|
||||||
|
{
|
||||||
|
emit newNotification(tr("Canceled file selection"));
|
||||||
|
}
|
||||||
|
|
||||||
QStringList Controller::pasteEntries(const QModelIndex &destinationParent, int row)
|
QStringList Controller::pasteEntries(const QModelIndex &destinationParent, int row)
|
||||||
{
|
{
|
||||||
if (m_cutEntries.isEmpty() || !m_entryModel.isNode(destinationParent)) {
|
if (m_cutEntries.isEmpty() || !m_entryModel.isNode(destinationParent)) {
|
||||||
return QStringList();
|
return QStringList();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (row < 0) {
|
if (row < 0) {
|
||||||
row = m_entryModel.rowCount(destinationParent);
|
row = m_entryModel.rowCount(destinationParent);
|
||||||
}
|
}
|
||||||
|
@ -198,7 +282,7 @@ QStringList Controller::pasteEntries(const QModelIndex &destinationParent, int r
|
||||||
bool Controller::copyToClipboard(const QString &text) const
|
bool Controller::copyToClipboard(const QString &text) const
|
||||||
{
|
{
|
||||||
#ifndef QT_NO_CLIPBOARD
|
#ifndef QT_NO_CLIPBOARD
|
||||||
auto *clipboard(QGuiApplication::clipboard());
|
auto *const clipboard(QGuiApplication::clipboard());
|
||||||
if (!clipboard) {
|
if (!clipboard) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -235,8 +319,23 @@ void Controller::setFileOpen(bool fileOpen)
|
||||||
|
|
||||||
void Controller::emitIoError(const QString &when)
|
void Controller::emitIoError(const QString &when)
|
||||||
{
|
{
|
||||||
const auto *const msg = catchIoFailure();
|
try {
|
||||||
emit fileError(tr("An IO error occured when %1 the file: ").arg(when) + QString::fromLocal8Bit(msg));
|
const auto *const msg = catchIoFailure();
|
||||||
|
emit fileError(tr("An IO error occured when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(msg));
|
||||||
|
} catch (const exception &e) {
|
||||||
|
emit fileError(tr("An unknown exception occured when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(e.what()));
|
||||||
|
} catch (...) {
|
||||||
|
emit fileError(tr("An unknown error occured when %1 the file %2.").arg(when, m_filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::setUseNativeFileDialog(bool useNativeFileDialog)
|
||||||
|
{
|
||||||
|
if (m_useNativeFileDialog == useNativeFileDialog) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit useNativeFileDialogChanged(m_useNativeFileDialog = useNativeFileDialog);
|
||||||
|
m_settings.setValue(QStringLiteral("usenativefiledialog"), m_useNativeFileDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QtGui
|
} // namespace QtGui
|
||||||
|
|
|
@ -29,6 +29,8 @@ class Controller : public QObject {
|
||||||
Q_PROPERTY(QList<QPersistentModelIndex> cutEntries READ cutEntries WRITE setCutEntries NOTIFY cutEntriesChanged)
|
Q_PROPERTY(QList<QPersistentModelIndex> cutEntries READ cutEntries WRITE setCutEntries NOTIFY cutEntriesChanged)
|
||||||
Q_PROPERTY(bool canPaste READ canPaste NOTIFY cutEntriesChanged)
|
Q_PROPERTY(bool canPaste READ canPaste NOTIFY cutEntriesChanged)
|
||||||
Q_PROPERTY(QStringList recentFiles READ recentFiles NOTIFY recentFilesChanged)
|
Q_PROPERTY(QStringList recentFiles READ recentFiles NOTIFY recentFilesChanged)
|
||||||
|
Q_PROPERTY(bool useNativeFileDialog READ useNativeFileDialog WRITE setUseNativeFileDialog NOTIFY useNativeFileDialogChanged)
|
||||||
|
Q_PROPERTY(bool supportsNativeFileDialog READ supportsNativeFileDialog NOTIFY supportsNativeFileDialogChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
|
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
|
||||||
|
@ -53,6 +55,9 @@ public:
|
||||||
Q_INVOKABLE bool copyToClipboard(const QString &text) const;
|
Q_INVOKABLE bool copyToClipboard(const QString &text) const;
|
||||||
bool canPaste() const;
|
bool canPaste() const;
|
||||||
const QStringList &recentFiles() const;
|
const QStringList &recentFiles() const;
|
||||||
|
bool useNativeFileDialog() const;
|
||||||
|
void setUseNativeFileDialog(bool useNativeFileDialog);
|
||||||
|
bool supportsNativeFileDialog() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void init();
|
void init();
|
||||||
|
@ -60,6 +65,12 @@ public slots:
|
||||||
void create(const QString &filePath = QString());
|
void create(const QString &filePath = QString());
|
||||||
void close();
|
void close();
|
||||||
void save();
|
void save();
|
||||||
|
bool showNativeFileDialog(bool existing);
|
||||||
|
void handleFileSelectionAccepted(const QString &filePath, bool existing);
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
void handleFileSelectionAcceptedDescriptor(const QString &nativeUrl, const QString &fileName, int fileDescriptor, bool existing);
|
||||||
|
#endif
|
||||||
|
void handleFileSelectionCanceled();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void filePathChanged(const QString &newFilePath);
|
void filePathChanged(const QString &newFilePath);
|
||||||
|
@ -75,6 +86,9 @@ signals:
|
||||||
void currentAccountChanged();
|
void currentAccountChanged();
|
||||||
void cutEntriesChanged(const QList<QPersistentModelIndex> &cutEntries);
|
void cutEntriesChanged(const QList<QPersistentModelIndex> &cutEntries);
|
||||||
void recentFilesChanged(const QStringList &recentFiles);
|
void recentFilesChanged(const QStringList &recentFiles);
|
||||||
|
void newNotification(const QString &message);
|
||||||
|
void useNativeFileDialogChanged(bool useNativeFileDialog);
|
||||||
|
void supportsNativeFileDialogChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resetFileStatus();
|
void resetFileStatus();
|
||||||
|
@ -93,8 +107,10 @@ private:
|
||||||
FieldModel m_fieldModel;
|
FieldModel m_fieldModel;
|
||||||
QList<QPersistentModelIndex> m_cutEntries;
|
QList<QPersistentModelIndex> m_cutEntries;
|
||||||
QStringList m_recentFiles;
|
QStringList m_recentFiles;
|
||||||
|
QString m_nativeUrl;
|
||||||
bool m_fileOpen;
|
bool m_fileOpen;
|
||||||
bool m_fileModified;
|
bool m_fileModified;
|
||||||
|
bool m_useNativeFileDialog;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline const QString &Controller::filePath() const
|
inline const QString &Controller::filePath() const
|
||||||
|
@ -178,6 +194,20 @@ inline const QStringList &Controller::recentFiles() const
|
||||||
return m_recentFiles;
|
return m_recentFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool Controller::useNativeFileDialog() const
|
||||||
|
{
|
||||||
|
return m_useNativeFileDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool Controller::supportsNativeFileDialog() const
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QtGui
|
} // namespace QtGui
|
||||||
|
|
||||||
#endif // QT_QUICK_GUI_CONTROLLER_H
|
#endif // QT_QUICK_GUI_CONTROLLER_H
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
#include "./initiatequick.h"
|
#include "./initiatequick.h"
|
||||||
#include "./controller.h"
|
#include "./controller.h"
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
#include "./android.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "resources/config.h"
|
#include "resources/config.h"
|
||||||
|
|
||||||
|
@ -8,11 +11,11 @@
|
||||||
#include <qtutilities/resources/resources.h>
|
#include <qtutilities/resources/resources.h>
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QIcon>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QTextCodec>
|
#include <QTextCodec>
|
||||||
#include <QIcon>
|
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
|
||||||
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
|
||||||
|
@ -36,12 +39,17 @@ enum RelevantFlags {
|
||||||
DrawsSystemBarBackgrounds = 0x80000000,
|
DrawsSystemBarBackgrounds = 0x80000000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
} // namespace WindowManager
|
||||||
}
|
} // namespace Android
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, const QString &file)
|
int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, const QString &file)
|
||||||
{
|
{
|
||||||
|
// setup logging for Android
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
qInstallMessageHandler(writeToAndroidLog);
|
||||||
|
#endif
|
||||||
|
|
||||||
// init application
|
// init application
|
||||||
SET_QT_APPLICATION_INFO;
|
SET_QT_APPLICATION_INFO;
|
||||||
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
@ -81,6 +89,9 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
|
||||||
// init Quick GUI
|
// init Quick GUI
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
Controller controller(settings, file);
|
Controller controller(settings, file);
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
registerControllerForAndroid(&controller);
|
||||||
|
#endif
|
||||||
auto *const context(engine.rootContext());
|
auto *const context(engine.rootContext());
|
||||||
context->setContextProperty(QStringLiteral("userPaths"), userPaths);
|
context->setContextProperty(QStringLiteral("userPaths"), userPaths);
|
||||||
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
|
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);
|
||||||
|
|
Loading…
Reference in New Issue