diff --git a/src/app/applicationcontroller.h b/src/app/+android/ImportDialog.qml similarity index 50% copy from src/app/applicationcontroller.h copy to src/app/+android/ImportDialog.qml index fb5f178..2d2adb6 100644 --- a/src/app/applicationcontroller.h +++ b/src/app/+android/ImportDialog.qml @@ -1,46 +1,30 @@ /* Copyright (C) 2018 Volker Krause 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 . */ -#ifndef APPLICATIONCONTROLLER_H -#define APPLICATIONCONTROLLER_H - -#include - -class QGeoPositionInfo; -class QGeoPositionInfoSource; - -class ApplicationController : public QObject -{ - Q_OBJECT -public: - explicit ApplicationController(QObject *parent = nullptr); - ~ApplicationController(); - - Q_INVOKABLE void showOnMap(const QVariant &place); - Q_INVOKABLE bool canNavigateTo(const QVariant &place); - Q_INVOKABLE void navigateTo(const QVariant &place); - -private: -#ifndef Q_OS_ANDROID - void navigateTo(const QGeoPositionInfo &from, const QVariant &to); - - QGeoPositionInfoSource *m_positionSource = nullptr; - QMetaObject::Connection m_pendingNavigation; -#endif -}; - -#endif // APPLICATIONCONTROLLER_H +import QtQml 2.0 + +QtObject { + function importPass() + { + _appController.importFile(); + } + + function importReservation() + { + _appController.importFile(); + } +} diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 0f621ee..7a06dec 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,112 +1,115 @@ set(itinerary_srcs countryinformation.cpp pkpassmanager.cpp reservationmanager.cpp timelinemodel.cpp ) ecm_qt_declare_logging_category(itinerary_srcs HEADER logging.h IDENTIFIER Log CATEGORY_NAME org.kde.itinerary ) add_library(itinerary STATIC ${itinerary_srcs}) target_link_libraries(itinerary PUBLIC itinerary-weather KPim::Itinerary KPim::PkPass KF5::I18n Qt5::Network ) add_executable(itinerary-app main.cpp applicationcontroller.cpp countrymodel.cpp localizer.cpp pkpassimageprovider.cpp settings.cpp qml.qrc ) target_include_directories(itinerary-app PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(itinerary-app PRIVATE itinerary Qt5::Quick KF5::Contacts ) if (ANDROID) # explicitly add runtime dependencies and transitive link dependencies, # so androiddeployqt picks them up target_link_libraries(itinerary-app PRIVATE KF5::Archive KF5::Kirigami2 Qt5::AndroidExtras Qt5::Svg KF5::Prison ) kirigami_package_breeze_icons(ICONS document-open edit-delete go-next-symbolic map-symbolic settings-configure view-calendar-day view-refresh weather-clear weather-clear-night weather-few-clouds weather-few-clouds-night weather-clouds weather-clouds-night weather-showers-day weather-showers-night weather-showers-scattered-day weather-showers-scattered-night weather-snow-scattered-day weather-snow-scattered-night weather-storm-day weather-storm-night weather-many-clouds weather-fog weather-showers weather-showers-scattered weather-hail weather-snow weather-snow-scattered weather-storm ) else () target_link_libraries(itinerary-app PRIVATE Qt5::Positioning) set_target_properties(itinerary-app PROPERTIES OUTPUT_NAME "itinerary") endif() qml_lint( main.qml BoardingPass.qml BusDelegate.qml BusPage.qml CountryInfoDelegate.qml DetailsPage.qml FlightDelegate.qml FlightPage.qml HotelDelegate.qml HotelPage.qml + ImportDialog.qml PkPassPage.qml PlaceDelegate.qml RestaurantDelegate.qml RestaurantPage.qml SettingsPage.qml TicketTokenDelegate.qml TimelineDelegate.qml TimelinePage.qml TouristAttractionDelegate.qml TrainDelegate.qml TrainPage.qml WeatherForecastDelegate.qml + + +android/ImportDialog.qml ) install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS}) if (NOT ANDROID) install(PROGRAMS org.kde.itinerary.desktop DESTINATION ${KDE_INSTALL_APPDIR}) endif() diff --git a/src/app/applicationcontroller.h b/src/app/ImportDialog.qml similarity index 50% copy from src/app/applicationcontroller.h copy to src/app/ImportDialog.qml index fb5f178..4d849c0 100644 --- a/src/app/applicationcontroller.h +++ b/src/app/ImportDialog.qml @@ -1,46 +1,47 @@ /* Copyright (C) 2018 Volker Krause 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 . */ -#ifndef APPLICATIONCONTROLLER_H -#define APPLICATIONCONTROLLER_H - -#include - -class QGeoPositionInfo; -class QGeoPositionInfoSource; - -class ApplicationController : public QObject -{ - Q_OBJECT -public: - explicit ApplicationController(QObject *parent = nullptr); - ~ApplicationController(); - - Q_INVOKABLE void showOnMap(const QVariant &place); - Q_INVOKABLE bool canNavigateTo(const QVariant &place); - Q_INVOKABLE void navigateTo(const QVariant &place); - -private: -#ifndef Q_OS_ANDROID - void navigateTo(const QGeoPositionInfo &from, const QVariant &to); - - QGeoPositionInfoSource *m_positionSource = nullptr; - QMetaObject::Connection m_pendingNavigation; -#endif -}; - -#endif // APPLICATIONCONTROLLER_H +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); + } + } +} diff --git a/src/app/applicationcontroller.cpp b/src/app/applicationcontroller.cpp index a0647e8..47d9355 100644 --- a/src/app/applicationcontroller.cpp +++ b/src/app/applicationcontroller.cpp @@ -1,199 +1,277 @@ /* Copyright (C) 2018 Volker Krause 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 . */ #include "applicationcontroller.h" +#include "logging.h" +#include "pkpassmanager.h" +#include "reservationmanager.h" #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #else #include #include #endif 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()) { return; } const auto geo = JsonLdDocument::readProperty(place, "geo").value(); const auto addr = JsonLdDocument::readProperty(place, "address").value(); #ifdef Q_OS_ANDROID QString intentUri; if (geo.isValid()) { intentUri = QLatin1String("geo:") + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()); } else if (!addr.isEmpty()) { intentUri = QLatin1String("geo:0,0?q=") + addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); } else { return; } const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("launchViewIntentFromUri", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(intentUri).object()); } #else if (geo.isValid()) { // zoom out further from airports, they are larger and you usually want to go further away from them const auto zoom = place.userType() == qMetaTypeId() ? 12 : 17; QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/")); const QString fragment = QLatin1String("map=") + QString::number(zoom) + QLatin1Char('/') + QString::number(geo.latitude()) + QLatin1Char('/') + QString::number(geo.longitude()); url.setFragment(fragment); QDesktopServices::openUrl(url); return; } if (!addr.isEmpty()) { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/search")); const QString queryString = addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); QUrlQuery query; query.addQueryItem(QStringLiteral("query"), queryString); url.setQuery(query); QDesktopServices::openUrl(url); } #endif } bool ApplicationController::canNavigateTo(const QVariant& place) { if (place.isNull()) { return false; } if (JsonLdDocument::readProperty(place, "geo").value().isValid()) { return true; } #ifdef Q_OS_ANDROID return !JsonLdDocument::readProperty(place, "address").value().isEmpty(); #else return false; #endif } void ApplicationController::navigateTo(const QVariant& place) { if (place.isNull()) { return; } #ifdef Q_OS_ANDROID const auto geo = JsonLdDocument::readProperty(place, "geo").value(); const auto addr = JsonLdDocument::readProperty(place, "address").value(); QString intentUri; if (geo.isValid()) { intentUri = QLatin1String("google.navigation:q=") + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude()); } else if (!addr.isEmpty()) { intentUri = QLatin1String("google.navigation:q=") + addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); } else { return; } const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("launchViewIntentFromUri", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(intentUri).object()); } #else if (m_pendingNavigation) { return; } if (!m_positionSource) { m_positionSource = QGeoPositionInfoSource::createDefaultSource(this); if (!m_positionSource) { qWarning() << "no geo position info source available"; return; } } if (m_positionSource->lastKnownPosition().isValid()) { navigateTo(m_positionSource->lastKnownPosition(), place); } else { m_pendingNavigation = connect(m_positionSource, &QGeoPositionInfoSource::positionUpdated, this, [this, place](const QGeoPositionInfo &pos) { navigateTo(pos, place); }); m_positionSource->requestUpdate(); } #endif } #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; } const auto geo = JsonLdDocument::readProperty(to, "geo").value(); if (geo.isValid()) { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/directions")); QUrlQuery query; query.addQueryItem(QLatin1String("route"), QString::number(from.coordinate().latitude()) + QLatin1Char(',') + QString::number(from.coordinate().longitude()) + QLatin1Char(';') + QString::number(geo.latitude()) + QLatin1Char(',') + QString::number(geo.longitude())); url.setQuery(query); QDesktopServices::openUrl(url); return; } } #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()); + 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("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("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 +} diff --git a/src/app/applicationcontroller.h b/src/app/applicationcontroller.h index fb5f178..eb0bb9c 100644 --- a/src/app/applicationcontroller.h +++ b/src/app/applicationcontroller.h @@ -1,46 +1,74 @@ /* Copyright (C) 2018 Volker Krause 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 . */ #ifndef APPLICATIONCONTROLLER_H #define APPLICATIONCONTROLLER_H #include +#ifdef Q_OS_ANDROID +#include +#endif + +class PkPassManager; +class ReservationManager; + class QGeoPositionInfo; class QGeoPositionInfoSource; class ApplicationController : public QObject { Q_OBJECT 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 }; #endif // APPLICATIONCONTROLLER_H diff --git a/src/app/main.cpp b/src/app/main.cpp index db53f17..a4715f6 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,160 +1,145 @@ /* Copyright (C) 2018 Volker Krause 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 . */ #include "itinerary_version.h" #include "logging.h" #include "applicationcontroller.h" #include "countryinformation.h" #include "countrymodel.h" #include "localizer.h" #include "pkpassmanager.h" #include "timelinemodel.h" #include "pkpassimageprovider.h" #include "reservationmanager.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #endif #include #include #include #include -void handleViewIntent(PkPassManager *passMgr) +void handleViewIntent(ApplicationController *appController) { #ifdef Q_OS_ANDROID // handle opened files const auto activity = QtAndroid::androidActivity(); if (!activity.isValid()) 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()); - 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 } #ifdef Q_OS_ANDROID Q_DECL_EXPORT #endif int main(int argc, char **argv) { QCoreApplication::setApplicationName(QStringLiteral("kde-itinerary")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QGuiApplication::setApplicationDisplayName(i18n("KDE Itinerary")); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QGuiApplication app(argc, argv); QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("map-globe"))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument(QStringLiteral("file"), i18n("PkPass or JSON-LD file to import.")); 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()); timelineModel.setReservationManager(&resMgr); QObject::connect(&settings, &Settings::homeCountryIsoCodeChanged, &timelineModel, &TimelineModel::setHomeCountryIsoCode); WeatherForecastManager weatherForecastMgr; weatherForecastMgr.setAllowNetworkAccess(settings.weatherForecastEnabled()); QObject::connect(&settings, &Settings::weatherForecastEnabledChanged, &weatherForecastMgr, &WeatherForecastManager::setAllowNetworkAccess); timelineModel.setWeatherForecastManager(&weatherForecastMgr); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Barcode", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Field", {}); qRegisterMetaType(); qmlRegisterUncreatableType("org.kde.kitinerary", 1, 0, "Ticket", {}); qmlRegisterUncreatableMetaObject(KItinerary::KnowledgeDb::staticMetaObject, "org.kde.kitinerary", 1, 0, "KnowledgeDb", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "CountryInformation", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "TimelineModel", {}); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine*) -> QObject*{ return new Localizer; }); qmlRegisterType("org.kde.itinerary", 1, 0, "CountryModel"); QQmlApplicationEngine engine; engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr)); engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextProperty(QStringLiteral("_pkpassManager"), &passMgr); engine.rootContext()->setContextProperty(QStringLiteral("_reservationManager"), &resMgr); engine.rootContext()->setContextProperty(QStringLiteral("_timelineModel"), &timelineModel); engine.rootContext()->setContextProperty(QStringLiteral("_appController"), &appController); engine.rootContext()->setContextProperty(QStringLiteral("_settings"), &settings); engine.load(QStringLiteral(":/main.qml")); for (const auto &file : parser.positionalArguments()) { if (file.endsWith(QLatin1String(".pkpass"))) passMgr.importPass(QUrl::fromLocalFile(file)); else resMgr.importReservation(QUrl::fromLocalFile(file)); } - handleViewIntent(&passMgr); + handleViewIntent(&appController); return app.exec(); } diff --git a/src/app/main.qml b/src/app/main.qml index 1062032..2655b89 100644 --- a/src/app/main.qml +++ b/src/app/main.qml @@ -1,99 +1,82 @@ /* Copyright (C) 2018 Volker Krause 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 . */ 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 Kirigami.ApplicationWindow { title: i18n("KDE Itinerary") header: Kirigami.ApplicationHeader {} 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 { title: i18n("KDE Itinerary") titleIcon: "map-symbolic" actions: [ 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") iconName: "view-refresh" onTriggered: { _pkpassManager.updatePasses(); } }, Kirigami.Action { text: i18n("Settings...") iconName: "settings-configure" onTriggered: pageStack.push(settingsComponent) } ] } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer } pageStack.initialPage: mainPageComponent Component { id: mainPageComponent App.TimelinePage {} } Component { id: pkpassComponent App.PkPassPage { pass: _pkpassManager.passObject(passId) } } Component { id: settingsComponent App.SettingsPage {} } } diff --git a/src/app/pkpassmanager.cpp b/src/app/pkpassmanager.cpp index cd1ccba..2bf9f8e 100644 --- a/src/app/pkpassmanager.cpp +++ b/src/app/pkpassmanager.cpp @@ -1,193 +1,193 @@ /* Copyright (C) 2018 Volker Krause 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 . */ #include "pkpassmanager.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include PkPassManager::PkPassManager(QObject* parent) : QObject(parent) , m_nam(new QNetworkAccessManager(this)) { } PkPassManager::~PkPassManager() = default; QVector PkPassManager::passes() const { const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); QDir::root().mkpath(basePath); QVector passIds; for (QDirIterator topIt(basePath, QDir::NoDotAndDotDot | QDir::Dirs); topIt.hasNext();) { for (QDirIterator subIt(topIt.next(), QDir::Files); subIt.hasNext();) { QFileInfo fi(subIt.next()); passIds.push_back(fi.dir().dirName() + QLatin1Char('/') + fi.baseName()); } } return passIds; } KPkPass::Pass* PkPassManager::pass(const QString& passId) { const auto it = m_passes.constFind(passId); if (it != m_passes.constEnd() && it.value()) { return it.value(); } const QString passPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/") + passId + QLatin1String(".pkpass"); if (!QFile::exists(passPath)) { return nullptr; } auto file = KPkPass::Pass::fromFile(passPath, this); // TODO error handling m_passes.insert(passId, file); return file; } QObject* PkPassManager::passObject(const QString& passId) { return pass(passId); } 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) { qCDebug(Log) << url << mode; if (!url.isLocalFile()) return; // TODO const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes"); QDir::root().mkpath(basePath); std::unique_ptr newPass(KPkPass::Pass::fromFile(url.toLocalFile())); if (!newPass) return; // TODO error handling if (newPass->passTypeIdentifier().isEmpty() || newPass->serialNumber().isEmpty()) return; // TODO error handling QDir dir(basePath); dir.mkdir(newPass->passTypeIdentifier()); dir.cd(newPass->passTypeIdentifier()); // serialNumber() can contain percent-encoding or slashes, // ie stuff we don't want to have in file names const auto serNum = QString::fromUtf8(newPass->serialNumber().toUtf8().toBase64(QByteArray::Base64UrlEncoding)); const QString passId = dir.dirName() + QLatin1Char('/') + serNum; auto oldPass = pass(passId); if (oldPass) { QFile::remove(dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); m_passes.remove(passId); } switch (mode) { case Move: QFile::rename(url.toLocalFile(), dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); break; case Copy: QFile::copy(url.toLocalFile(), dir.absoluteFilePath(serNum + QLatin1String(".pkpass"))); break; } if (oldPass) { // check for changes and generate change message QStringList changes; for (const auto &f : newPass->fields()) { const auto prevValue = oldPass->field(f.key()).value(); const auto curValue = f.value(); if (curValue != prevValue) { changes.push_back(f.changeMessage()); } } emit passUpdated(passId, changes); oldPass->deleteLater(); } else { emit passAdded(passId); } } void PkPassManager::removePass(const QString& passId) { const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/passes/"); QFile::remove(basePath + QLatin1Char('/') + passId + QLatin1String(".pkpass")); emit passRemoved(passId); delete m_passes.take(passId); } void PkPassManager::updatePass(const QString& passId) { auto p = pass(passId); if (!p || p->webServiceUrl().isEmpty() || p->authenticationToken().isEmpty()) return; if (relevantDate(p) < QDateTime::currentDateTimeUtc()) // TODO check expiration date and voided property return; QNetworkRequest req(p->passUpdateUrl()); req.setRawHeader("Authorization", "ApplePass " + p->authenticationToken().toUtf8()); req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); auto reply = m_nam->get(req); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) { qCWarning(Log) << "Failed to download pass:" << reply->errorString(); return; } QTemporaryFile tmp; tmp.open(); tmp.write(reply->readAll()); tmp.close(); - importPassFromTempFile(tmp.fileName()); + importPassFromTempFile(QUrl::fromLocalFile(tmp.fileName())); }); } void PkPassManager::updatePasses() { for (const auto &passId : passes()) updatePass(passId); } QDateTime PkPassManager::relevantDate(KPkPass::Pass *pass) { const auto dt = pass->relevantDate(); if (dt.isValid()) return dt; return pass->expirationDate(); } diff --git a/src/app/pkpassmanager.h b/src/app/pkpassmanager.h index bdaaf95..304a8be 100644 --- a/src/app/pkpassmanager.h +++ b/src/app/pkpassmanager.h @@ -1,64 +1,64 @@ /* Copyright (C) 2018 Volker Krause 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 . */ #ifndef PKPASSMANAGER_H #define PKPASSMANAGER_H #include #include class QNetworkAccessManager; namespace KPkPass { class Pass; } class PkPassManager : public QObject { Q_OBJECT public: PkPassManager(QObject *parent = nullptr); ~PkPassManager(); QVector passes() const; KPkPass::Pass* pass(const QString &passId); 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); Q_INVOKABLE void updatePasses(); static QDateTime relevantDate(KPkPass::Pass *pass); signals: void passAdded(const QString &passId); void passUpdated(const QString &passId, const QStringList &changes); void passRemoved(const QString &passId); private: enum ImportMode { Copy, Move }; void doImportPass(const QUrl &url, ImportMode mode); QHash m_passes; QNetworkAccessManager *m_nam; }; #endif // PKPASSMANAGER_H diff --git a/src/app/qml.qrc b/src/app/qml.qrc index b70db81..d8d5630 100644 --- a/src/app/qml.qrc +++ b/src/app/qml.qrc @@ -1,26 +1,29 @@ main.qml BoardingPass.qml BusDelegate.qml BusPage.qml CountryInfoDelegate.qml DetailsPage.qml FlightDelegate.qml FlightPage.qml HotelDelegate.qml HotelPage.qml + ImportDialog.qml PkPassPage.qml PlaceDelegate.qml RestaurantDelegate.qml RestaurantPage.qml SettingsPage.qml TicketTokenDelegate.qml TimelineDelegate.qml TimelinePage.qml TouristAttractionDelegate.qml TrainDelegate.qml TrainPage.qml WeatherForecastDelegate.qml + + +android/ImportDialog.qml