Commit 6778bad5 authored by Volker Krause's avatar Volker Krause

Add native Android file dialog support

This actually makes the file open dialog usable at all on Android.
parent f52ec177
/*
Copyright (C) 2018 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQml 2.0
QtObject {
function importPass()
{
_appController.importFile();
}
function importReservation()
{
_appController.importFile();
}
}
......@@ -92,6 +92,7 @@ qml_lint(
FlightPage.qml
HotelDelegate.qml
HotelPage.qml
ImportDialog.qml
PkPassPage.qml
PlaceDelegate.qml
RestaurantDelegate.qml
......@@ -104,6 +105,8 @@ qml_lint(
TrainDelegate.qml
TrainPage.qml
WeatherForecastDelegate.qml
+android/ImportDialog.qml
)
install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS})
......
/*
Copyright (C) 2018 Volker Krause <vkrause@kde.org>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.0
import QtQuick.Dialogs 1.0
Item {
function importReservation()
{
fileDialog.loadPass = false;
fileDialog.visible = true;
}
function importPass()
{
fileDialog.loadPass = true;
fileDialog.visible = true;
}
FileDialog {
property bool loadPass: false
id: fileDialog
title: i18n("Please choose a file")
folder: shortcuts.home
onAccepted: {
console.log(fileDialog.fileUrls);
if (loadPass)
_pkpassManager.importPass(fileDialog.fileUrl);
else
_reservationManager.importReservation(fileDialog.fileUrl);
}
}
}
......@@ -16,6 +16,9 @@
*/
#include "applicationcontroller.h"
#include "logging.h"
#include "pkpassmanager.h"
#include "reservationmanager.h"
#include <KItinerary/JsonLdDocument>
#include <KItinerary/Place>
......@@ -37,11 +40,24 @@ using namespace KItinerary;
ApplicationController::ApplicationController(QObject* parent)
: QObject(parent)
#ifdef Q_OS_ANDROID
, m_activityResultReceiver(this)
#endif
{
}
ApplicationController::~ApplicationController() = default;
void ApplicationController::setReservationManager(ReservationManager* resMgr)
{
m_resMgr = resMgr;
}
void ApplicationController::setPkPassManager(PkPassManager* pkPassMgr)
{
m_pkPassMgr = pkPassMgr;
}
void ApplicationController::showOnMap(const QVariant &place)
{
if (place.isNull()) {
......@@ -176,6 +192,7 @@ void ApplicationController::navigateTo(const QVariant& place)
#ifndef Q_OS_ANDROID
void ApplicationController::navigateTo(const QGeoPositionInfo &from, const QVariant &to)
{
qDebug() << from.coordinate() << from.isValid();
disconnect(m_pendingNavigation);
if (!from.isValid()) {
return;
......@@ -197,3 +214,64 @@ void ApplicationController::navigateTo(const QGeoPositionInfo &from, const QVari
}
}
#endif
#ifdef Q_OS_ANDROID
static bool isPkPassFile(const QUrl &url)
{
// ### is this enough, or do we need to check the file magic?
return url.fileName().endsWith(QLatin1String(".pkpass"));
}
void ApplicationController::importFromIntent(const QAndroidJniObject &intent)
{
if (!intent.isValid()) {
return;
}
const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;");
if (!uri.isValid()) {
return;
}
const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;");
qCDebug(Log) << uri.callObjectMethod("toString", "()Ljava/lang/String;").toString();
if (scheme.toString() == QLatin1String("content")) {
const auto tmpFile = QtAndroid::androidActivity().callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object<jobject>());
const auto tmpUrl = QUrl::fromLocalFile(tmpFile.toString());
if (isPkPassFile(tmpUrl)) {
m_pkPassMgr->importPassFromTempFile(tmpUrl);
} else {
m_resMgr->importReservation(tmpUrl);
}
} else if (scheme.toString() == QLatin1String("file")) {
const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;");
const auto url = QUrl(uriStr.toString());
if (isPkPassFile(url)) {
m_pkPassMgr->importPass(url);
} else {
m_resMgr->importReservation(url);
}
} else {
const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;");
qCWarning(Log) << "Unknown intent URI:" << uriStr.toString();
}
}
void ApplicationController::ActivityResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &intent)
{
qCDebug(Log) << receiverRequestCode << resultCode;
m_controller->importFromIntent(intent);
}
#endif
void ApplicationController::importFile()
{
#ifdef Q_OS_ANDROID
const auto ACTION_OPEN_DOCUMENT = QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "ACTION_OPEN_DOCUMENT");
QAndroidJniObject intent("android/content/Intent", "(Ljava/lang/String;)V", ACTION_OPEN_DOCUMENT.object());
const auto CATEGORY_OPENABLE = QAndroidJniObject::getStaticObjectField<jstring>("android/content/Intent", "CATEGORY_OPENABLE");
intent.callObjectMethod("addCategory", "(Ljava/lang/String;)Landroid/content/Intent;", CATEGORY_OPENABLE.object());
intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString(QStringLiteral("*/*")).object());
QtAndroid::startActivity(intent, 0, &m_activityResultReceiver);
#endif
}
......@@ -20,6 +20,13 @@
#include <QObject>
#ifdef Q_OS_ANDROID
#include <QAndroidActivityResultReceiver>
#endif
class PkPassManager;
class ReservationManager;
class QGeoPositionInfo;
class QGeoPositionInfoSource;
......@@ -30,16 +37,37 @@ public:
explicit ApplicationController(QObject *parent = nullptr);
~ApplicationController();
void setReservationManager(ReservationManager *resMgr);
void setPkPassManager(PkPassManager *pkPassMgr);
Q_INVOKABLE void showOnMap(const QVariant &place);
Q_INVOKABLE bool canNavigateTo(const QVariant &place);
Q_INVOKABLE void navigateTo(const QVariant &place);
Q_INVOKABLE void importFile();
#ifdef Q_OS_ANDROID
void importFromIntent(const QAndroidJniObject &intent);
#endif
private:
ReservationManager *m_resMgr = nullptr;
PkPassManager *m_pkPassMgr = nullptr;
#ifndef Q_OS_ANDROID
void navigateTo(const QGeoPositionInfo &from, const QVariant &to);
QGeoPositionInfoSource *m_positionSource = nullptr;
QMetaObject::Connection m_pendingNavigation;
#else
class ActivityResultReceiver : public QAndroidActivityResultReceiver {
public:
explicit inline ActivityResultReceiver(ApplicationController *controller)
: m_controller(controller) {}
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &intent) override;
private:
ApplicationController *m_controller;
};
ActivityResultReceiver m_activityResultReceiver;
#endif
};
......
......@@ -52,7 +52,7 @@
#include <QGuiApplication>
#include <QIcon>
void handleViewIntent(PkPassManager *passMgr)
void handleViewIntent(ApplicationController *appController)
{
#ifdef Q_OS_ANDROID
// handle opened files
......@@ -61,26 +61,9 @@ void handleViewIntent(PkPassManager *passMgr)
return;
const auto intent = activity.callObjectMethod("getIntent", "()Landroid/content/Intent;");
if (!intent.isValid())
return;
const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;");
if (!uri.isValid())
return;
const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;");
if (scheme.toString() == QLatin1String("content")) {
const auto tmpFile = activity.callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object<jobject>());
passMgr->importPassFromTempFile(tmpFile.toString());
} else if (scheme.toString() == QLatin1String("file")) {
const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;");
passMgr->importPass(QUrl(uriStr.toString()));
} else {
const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;");
qCWarning(Log) << "Unknown intent URI:" << uriStr.toString();
}
appController->importFromIntent(intent);
#else
Q_UNUSED(passMgr);
Q_UNUSED(appController);
#endif
}
......@@ -108,10 +91,12 @@ int main(int argc, char **argv)
parser.process(app);
Settings settings;
ApplicationController appController;
PkPassManager passMgr;
ReservationManager resMgr;
resMgr.setPkPassManager(&passMgr);
ApplicationController appController;
appController.setReservationManager(&resMgr);
appController.setPkPassManager(&passMgr);
TimelineModel timelineModel;
timelineModel.setHomeCountryIsoCode(settings.homeCountryIsoCode());
......@@ -154,7 +139,7 @@ int main(int argc, char **argv)
else
resMgr.importReservation(QUrl::fromLocalFile(file));
}
handleViewIntent(&passMgr);
handleViewIntent(&appController);
return app.exec();
}
......@@ -17,7 +17,6 @@
import QtQuick 2.5
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.0
import QtQuick.Controls 2.1 as QQC2
import org.kde.kirigami 2.0 as Kirigami
import "." as App
......@@ -29,18 +28,8 @@ Kirigami.ApplicationWindow {
width: 480
height: 720
FileDialog {
property bool loadPass: false
id: fileDialog
title: i18n("Please choose a file")
folder: shortcuts.home
onAccepted: {
console.log(fileDialog.fileUrls);
if (loadPass)
_pkpassManager.importPass(fileDialog.fileUrl);
else
_reservationManager.importReservation(fileDialog.fileUrl);
}
App.ImportDialog {
id: importDialog
}
globalDrawer: Kirigami.GlobalDrawer {
......@@ -50,18 +39,12 @@ Kirigami.ApplicationWindow {
Kirigami.Action {
text: i18n("Import Reservation...")
iconName: "document-open"
onTriggered: {
fileDialog.loadPass = false;
fileDialog.visible = true;
}
onTriggered: importDialog.importReservation()
},
Kirigami.Action {
text: i18n("Import Pass...")
iconName: "document-open"
onTriggered: {
fileDialog.loadPass = true;
fileDialog.visible = true;
}
onTriggered: importDialog.importPass()
},
Kirigami.Action {
text: i18n("Check for Updates")
......
......@@ -83,9 +83,9 @@ void PkPassManager::importPass(const QUrl& url)
doImportPass(url, Copy);
}
void PkPassManager::importPassFromTempFile(const QString& tmpFile)
void PkPassManager::importPassFromTempFile(const QUrl& tmpFile)
{
doImportPass(QUrl::fromLocalFile(tmpFile), Move);
doImportPass(tmpFile, Move);
}
void PkPassManager::doImportPass(const QUrl& url, PkPassManager::ImportMode mode)
......@@ -174,7 +174,7 @@ void PkPassManager::updatePass(const QString& passId)
tmp.open();
tmp.write(reply->readAll());
tmp.close();
importPassFromTempFile(tmp.fileName());
importPassFromTempFile(QUrl::fromLocalFile(tmp.fileName()));
});
}
......
......@@ -40,7 +40,7 @@ public:
Q_INVOKABLE QObject* passObject(const QString &passId);
Q_INVOKABLE void importPass(const QUrl &url);
void importPassFromTempFile(const QString &tmpFile);
void importPassFromTempFile(const QUrl &tmpFile);
Q_INVOKABLE void removePass(const QString &passId);
void updatePass(const QString &passId);
......
......@@ -10,6 +10,7 @@
<file>FlightPage.qml</file>
<file>HotelDelegate.qml</file>
<file>HotelPage.qml</file>
<file>ImportDialog.qml</file>
<file>PkPassPage.qml</file>
<file>PlaceDelegate.qml</file>
<file>RestaurantDelegate.qml</file>
......@@ -22,5 +23,7 @@
<file>TrainDelegate.qml</file>
<file>TrainPage.qml</file>
<file>WeatherForecastDelegate.qml</file>
<file>+android/ImportDialog.qml</file>
</qresource>
</RCC>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment