Compare commits

...

1 Commits

Author SHA1 Message Date
Martchus d3b95825df WIP: Show native file dialog under Android 2018-09-05 00:34:54 +02:00
10 changed files with 363 additions and 22 deletions

View File

@ -71,6 +71,14 @@ set(QML_SRC_FILES
resources/icons.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
translations/${META_PROJECT_NAME}_de_DE.ts

View File

@ -3,7 +3,7 @@
<application android:icon="@drawable/icon" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="@string/app_name">
<activity
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:screenOrientation="unspecified"
android:theme="@style/AppTheme">
@ -36,7 +36,7 @@
<!-- Splash screen -->
</activity>
</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"/>
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.

61
android/build.gradle Normal file
View File

@ -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
}
}

View File

@ -0,0 +1,67 @@
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;
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;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_PICK_EXISTING_FILE:
case REQUEST_CODE_PICK_NEW_FILE:
if (resultCode != RESULT_OK) {
onAndroidFileDialogRejected();
return;
}
boolean existingFile = requestCode == REQUEST_CODE_PICK_EXISTING_FILE;
Uri uri = data.getData();
if (uri != null) {
try {
DocumentFile file = DocumentFile.fromSingleUri(this, uri);
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(file.getUri(), existingFile ? "rw" : "wt");
onAndroidFileDialogAcceptedHandle(uri.toString(), fd.getFd(), existingFile);
} catch (FileNotFoundException e) {
onAndroidError("Failed to open 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 onAndroidFileDialogAcceptedHandle(String fileName, int fileHandle, boolean existing);
public static native void onAndroidFileDialogRejected();
}

View File

@ -71,6 +71,12 @@ Kirigami.ApplicationWindow {
onTriggered: nativeInterface.close()
}
]
Controls.Switch {
text: qsTr("Use native file dialog")
checked: nativeInterface.useNativeFileDialog
visible: nativeInterface.supportsNativeFileDialog
onCheckedChanged: nativeInterface.useNativeFileDialog = checked
}
Controls.Switch {
id: showPasswordsOnFocusSwitch
text: qsTr("Show passwords on focus")
@ -97,27 +103,31 @@ Kirigami.ApplicationWindow {
id: fileDialog
title: selectExisting ? qsTr("Select an existing file") : qsTr(
"Select path for new file")
property bool selectExisting: true
onAccepted: {
if (fileUrls.length < 1) {
return
}
if (selectExisting) {
nativeInterface.load(fileUrls[0])
} else {
nativeInterface.create(fileUrls[0])
}
}
onRejected: {
showPassiveNotification(qsTr("Canceled file selection"))
nativeInterface.handleFileSelectionAccepted(fileUrls[0],
this.selectExisting)
}
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() {
this.selectExisting = true
this.open()
this.show()
}
function createNew() {
this.selectExisting = false
this.open()
this.show()
}
}
@ -149,6 +159,9 @@ Kirigami.ApplicationWindow {
showPassiveNotification(qsTr("%1 saved").arg(
nativeInterface.fileName))
}
onNewNotification: {
showPassiveNotification(message)
}
}
Component {
@ -173,17 +186,17 @@ Kirigami.ApplicationWindow {
function pushStackEntry(entryModel, rootIndex) {
pageStack.push(entriesComponent.createObject(root, {
entryModel: entryModel,
rootIndex: rootIndex,
title: entryModel.data(
rootIndex)
"entryModel": entryModel,
"rootIndex": rootIndex,
"title": entryModel.data(
rootIndex)
}))
}
function createFileActions(files) {
return files.map(function (filePath) {
return this.createObject(root, {
filePath: filePath
"filePath": filePath
})
}, fileActionComponent)
}

76
quickgui/android.cpp Normal file
View File

@ -0,0 +1,76 @@
#include "./android.h"
#include "./controller.h"
#include <QtAndroid>
#include <QAndroidJniObject>
#include <QMetaObject>
#include <jni.h>
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);
}
}
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 onAndroidFileDialogAcceptedHandle(JNIEnv *, jobject, jstring fileName, jint fileHandle, jboolean existing)
{
QMetaObject::invokeMethod(QtGui::controllerForAndroid, "handleFileSelectionAcceptedDescriptor", Qt::QueuedConnection, 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)},
{"onAndroidFileDialogAcceptedHandle", "(Ljava/lang/String;IZ)V", reinterpret_cast<void *>(onAndroidFileDialogAcceptedHandle)},
{"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;
}

12
quickgui/android.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef QT_QUICK_GUI_ANDROID_H
#define QT_QUICK_GUI_ANDROID_H
namespace QtGui {
class Controller;
void registerControllerForAndroid(Controller *controller);
bool showAndroidFileDialog(bool existing);
}
#endif // QT_QUICK_GUI_ANDROID_H

View File

@ -1,4 +1,5 @@
#include "./controller.h"
#include "./android.h"
#include <passwordfile/io/cryptoexception.h>
#include <passwordfile/io/parsingexception.h>
@ -6,6 +7,7 @@
#include <qtutilities/misc/dialogutils.h>
#include <c++utilities/io/catchiofailure.h>
#include <c++utilities/io/nativefilestream.h>
#include <c++utilities/io/path.h>
#ifndef QT_NO_CLIPBOARD
@ -32,12 +34,14 @@ Controller::Controller(QSettings &settings, const QString &filePath, QObject *pa
, m_settings(settings)
, m_fileOpen(false)
, m_fileModified(false)
, m_useNativeFileDialog(false)
{
m_entryFilterModel.setSourceModel(&m_entryModel);
// share settings with main window
m_settings.beginGroup(QStringLiteral("mainwindow"));
m_recentFiles = m_settings.value(QStringLiteral("recententries")).toStringList();
m_useNativeFileDialog = m_settings.value(QStringLiteral("usenativefiledialog"), m_useNativeFileDialog).toBool();
// set initial file path
setFilePath(filePath);
@ -66,6 +70,11 @@ void Controller::setFilePath(const QString &filePath)
emit filePathChanged(m_filePath = filePath);
// handle recent files
#ifdef Q_OS_ANDROID
if (m_useNativeFileDialog) {
return; // native file dialog under Android makes it impossible to store URIs persistently
}
#endif
auto index = m_recentFiles.indexOf(m_filePath);
if (!index) {
return;
@ -131,7 +140,10 @@ void Controller::create(const QString &filePath)
resetFileStatus();
try {
m_file.create();
if (!m_file.isOpen()) {
m_file.create();
}
m_file.generateRootEntry();
m_entryModel.setRootEntry(m_file.rootEntry());
setFileOpen(true);
updateWindowTitle();
@ -170,6 +182,56 @@ 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)
{
if (existing) {
load(filePath);
} else {
create(filePath);
}
}
#if defined(Q_OS_ANDROID) && defined(CPP_UTILITIES_USE_NATIVE_FILE_BUFFER)
void Controller::handleFileSelectionAcceptedDescriptor(const QString &filePath, int fileDescriptor, bool existing)
{
try {
m_file.setPath(filePath.toStdString());
m_file.fileStream().openFromFileDescriptor(fileDescriptor, ios_base::in | ios_base::binary);
m_file.fileStream().seekg(0);
emitIoError("seeked to begin");
char buf[4];
m_file.fileStream().read(buf, 4);
emitIoError("first 4 byte: " + QString::fromLocal8Bit(buf, 4));
m_file.opened();
} catch (...) {
emitIoError(tr("opening from native file descriptor"));
}
handleFileSelectionAccepted(filePath, existing);
}
#endif
void Controller::handleFileSelectionCanceled()
{
emit newNotification(tr("Canceled file selection"));
}
QStringList Controller::pasteEntries(const QModelIndex &destinationParent, int row)
{
if (m_cutEntries.isEmpty() || !m_entryModel.isNode(destinationParent)) {
@ -236,7 +298,7 @@ void Controller::setFileOpen(bool fileOpen)
void Controller::emitIoError(const QString &when)
{
const auto *const msg = catchIoFailure();
emit fileError(tr("An IO error occured when %1 the file: ").arg(when) + QString::fromLocal8Bit(msg));
emit fileError(tr("An IO error occured when %1 the file %2: ").arg(when, m_filePath) + QString::fromLocal8Bit(msg));
}
} // namespace QtGui

View File

@ -29,6 +29,8 @@ class Controller : public QObject {
Q_PROPERTY(QList<QPersistentModelIndex> cutEntries READ cutEntries WRITE setCutEntries NOTIFY cutEntriesChanged)
Q_PROPERTY(bool canPaste READ canPaste NOTIFY cutEntriesChanged)
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:
explicit Controller(QSettings &settings, const QString &filePath = QString(), QObject *parent = nullptr);
@ -53,6 +55,9 @@ public:
Q_INVOKABLE bool copyToClipboard(const QString &text) const;
bool canPaste() const;
const QStringList &recentFiles() const;
bool useNativeFileDialog() const;
void setUseNativeFileDialog(bool useNativeFileDialog);
bool supportsNativeFileDialog() const;
public slots:
void init();
@ -60,6 +65,12 @@ public slots:
void create(const QString &filePath = QString());
void close();
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 &filePath, int fileDescriptor, bool existing);
#endif
void handleFileSelectionCanceled();
signals:
void filePathChanged(const QString &newFilePath);
@ -75,6 +86,9 @@ signals:
void currentAccountChanged();
void cutEntriesChanged(const QList<QPersistentModelIndex> &cutEntries);
void recentFilesChanged(const QStringList &recentFiles);
void newNotification(const QString &message);
void useNativeFileDialogChanged(bool useNativeFileDialog);
void supportsNativeFileDialogChanged();
private:
void resetFileStatus();
@ -95,6 +109,7 @@ private:
QStringList m_recentFiles;
bool m_fileOpen;
bool m_fileModified;
bool m_useNativeFileDialog;
};
inline const QString &Controller::filePath() const
@ -178,6 +193,27 @@ inline const QStringList &Controller::recentFiles() const
return m_recentFiles;
}
inline bool Controller::useNativeFileDialog() const
{
return m_useNativeFileDialog;
}
inline void Controller::setUseNativeFileDialog(bool useNativeFileDialog)
{
if (m_useNativeFileDialog != useNativeFileDialog) {
emit useNativeFileDialogChanged(m_useNativeFileDialog = 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
#endif // QT_QUICK_GUI_CONTROLLER_H

View File

@ -1,5 +1,8 @@
#include "./initiatequick.h"
#include "./controller.h"
#ifdef Q_OS_ANDROID
#include "./android.h"
#endif
#include "resources/config.h"
@ -8,11 +11,11 @@
#include <qtutilities/resources/resources.h>
#include <QGuiApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QSettings>
#include <QTextCodec>
#include <QIcon>
#include <QtQml>
#ifdef PASSWORD_MANAGER_GUI_QTWIDGETS
@ -36,8 +39,8 @@ enum RelevantFlags {
DrawsSystemBarBackgrounds = 0x80000000,
};
}
}
}
} // namespace WindowManager
} // namespace Android
#endif
int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, const QString &file)
@ -81,6 +84,9 @@ int runQuickGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs, c
// init Quick GUI
QQmlApplicationEngine engine;
Controller controller(settings, file);
#ifdef Q_OS_ANDROID
registerControllerForAndroid(&controller);
#endif
auto *const context(engine.rootContext());
context->setContextProperty(QStringLiteral("userPaths"), userPaths);
context->setContextProperty(QStringLiteral("nativeInterface"), &controller);