diff --git a/autotests/applicationcontrollertest.cpp b/autotests/applicationcontrollertest.cpp index acfcb38..b67db84 100644 --- a/autotests/applicationcontrollertest.cpp +++ b/autotests/applicationcontrollertest.cpp @@ -1,111 +1,114 @@ /* 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 #include #include #include #include #include #include class AppControllerTest : public QObject { Q_OBJECT private: void clearPasses(PkPassManager *mgr) { for (const auto id : mgr->passes()) mgr->removePass(id); } void clearReservations(ReservationManager *mgr) { for (const auto id : mgr->reservations()) { mgr->removeReservation(id); } } QByteArray readFile(const QString &fn) { QFile f(fn); f.open(QFile::ReadOnly); return f.readAll(); } private slots: void initTestCase() { qputenv("TZ", "UTC"); QStandardPaths::setTestModeEnabled(true); } void testImportData() { PkPassManager passMgr; clearPasses(&passMgr); QSignalSpy passSpy(&passMgr, &PkPassManager::passAdded); QVERIFY(passSpy.isValid()); ReservationManager resMgr; clearReservations(&resMgr); QSignalSpy resSpy(&resMgr, &ReservationManager::reservationAdded); QVERIFY(resSpy.isValid()); ApplicationController appController; appController.setPkPassManager(&passMgr); appController.setReservationManager(&resMgr); appController.importData(readFile(QLatin1String(SOURCE_DIR "/data/4U8465-v1.json"))); QCOMPARE(resSpy.size(), 1); QCOMPARE(passSpy.size(), 0); appController.importData(readFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass"))); QCOMPARE(resSpy.size(), 1); QCOMPARE(passSpy.size(), 1); + appController.importData("M1DOE/JOHN EXXX007 TXLBRUSN 2592 110Y"); + QCOMPARE(resSpy.size(), 2); + QCOMPARE(passSpy.size(), 1); // TODO PDF } void testImportFile() { PkPassManager passMgr; clearPasses(&passMgr); QSignalSpy passSpy(&passMgr, &PkPassManager::passAdded); QVERIFY(passSpy.isValid()); ReservationManager resMgr; clearReservations(&resMgr); QSignalSpy resSpy(&resMgr, &ReservationManager::reservationAdded); QVERIFY(resSpy.isValid()); ApplicationController appController; appController.setPkPassManager(&passMgr); appController.setReservationManager(&resMgr); appController.importFromUrl(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/4U8465-v1.json"))); QCOMPARE(resSpy.size(), 1); QCOMPARE(passSpy.size(), 0); appController.importFromUrl(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/boardingpass-v1.pkpass"))); QCOMPARE(resSpy.size(), 1); QCOMPARE(passSpy.size(), 1); // TODO PDF } }; QTEST_GUILESS_MAIN(AppControllerTest) #include "applicationcontrollertest.moc" diff --git a/src/app/applicationcontroller.cpp b/src/app/applicationcontroller.cpp index f2345c0..9dfdcc4 100644 --- a/src/app/applicationcontroller.cpp +++ b/src/app/applicationcontroller.cpp @@ -1,458 +1,468 @@ /* 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 "contenttypeprober.h" #include "logging.h" #include "pkpassmanager.h" #include "reservationmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #else #include #include #endif #include using namespace KItinerary; #ifdef Q_OS_ANDROID static void importReservation(JNIEnv *env, jobject that, jstring data) { Q_UNUSED(that); ApplicationController::instance()->importData(env->GetStringUTFChars(data, 0)); } static const JNINativeMethod methods[] = { {"importReservation", "(Ljava/lang/String;)V", (void*)importReservation} }; Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void*) { static bool initialized = false; if (initialized) return JNI_VERSION_1_6; initialized = true; JNIEnv *env = nullptr; if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) { qCWarning(Log) << "Failed to get JNI environment."; return -1; } jclass cls = env->FindClass("org/kde/itinerary/Activity"); if (env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) { qCWarning(Log) << "Failed to register native functions."; return -1; } return JNI_VERSION_1_4; } #endif ApplicationController* ApplicationController::s_instance = nullptr; ApplicationController::ApplicationController(QObject* parent) : QObject(parent) #ifdef Q_OS_ANDROID , m_activityResultReceiver(this) #endif { s_instance = this; connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &ApplicationController::clipboardContentChanged); } ApplicationController::~ApplicationController() { s_instance = nullptr; } ApplicationController* ApplicationController::instance() { return s_instance; } 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 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()); importLocalFile(tmpUrl, true); return; } const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); importFromUrl(QUrl(uriStr.toString())); } void ApplicationController::ActivityResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &intent) { qCDebug(Log) << receiverRequestCode << resultCode; m_controller->importFromIntent(intent); } #endif void ApplicationController::showImportFileDialog() { #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 } void ApplicationController::importFromClipboard() { if (QGuiApplication::clipboard()->mimeData()->hasUrls()) { const auto urls = QGuiApplication::clipboard()->mimeData()->urls(); for (const auto url : urls) importFromUrl(url); + return; } if (QGuiApplication::clipboard()->mimeData()->hasText()) { - const auto content = QGuiApplication::clipboard()->text(); - const auto data = IataBcbpParser::parse(content, QDate::currentDate()); - ExtractorPostprocessor postproc; - postproc.setContextDate(QDateTime::currentDateTime()); - postproc.process(data); - for (const auto &res : postproc.result()) - m_resMgr->addReservation(res); - - return; + const auto content = QGuiApplication::clipboard()->mimeData()->data(QLatin1String("text/plain")); + importData(content); } } void ApplicationController::importFromUrl(const QUrl &url) { qCDebug(Log) << url; if (url.isLocalFile()) { importLocalFile(url, false); return; } if (url.scheme().startsWith(QLatin1String("http"))) { if (!m_nam ) { m_nam = new QNetworkAccessManager(this); } auto reqUrl(url); reqUrl.setScheme(QLatin1String("https")); QNetworkRequest req(reqUrl); req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); auto reply = m_nam->get(req); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) { qCDebug(Log) << reply->url() << reply->errorString(); return; } importData(reply->readAll()); }); return; } qCDebug(Log) << "Unhandled URL type:" << url; } void ApplicationController::importLocalFile(const QUrl &url, bool isTempFile) { qCDebug(Log) << url; if (url.isEmpty() || !url.isLocalFile()) return; QFile f(url.toLocalFile()); if (!f.open(QFile::ReadOnly)) { qCWarning(Log) << "Failed to open" << url << f.errorString(); return; } const auto head = f.peek(4); const auto type = ContentTypeProber::probe(head, url); switch (type) { case ContentTypeProber::Unknown: qCWarning(Log) << "Unknown content type:" << url; break; case ContentTypeProber::PkPass: if (isTempFile) m_pkPassMgr->importPassFromTempFile(url); else m_pkPassMgr->importPass(url); break; case ContentTypeProber::JsonLd: m_resMgr->importReservation(f.readAll()); break; case ContentTypeProber::PDF: if (f.size() <= 4000000) importPdf(f.readAll()); break; + case ContentTypeProber::IataBcbp: + importIataBcbp(f.readAll()); + break; } } void ApplicationController::importData(const QByteArray &data) { qCDebug(Log); const auto type = ContentTypeProber::probe(data); switch (type) { case ContentTypeProber::Unknown: qCWarning(Log) << "Unknown content type for received data."; break; case ContentTypeProber::PkPass: m_pkPassMgr->importPassFromData(data); break; case ContentTypeProber::JsonLd: m_resMgr->importReservation(data); break; case ContentTypeProber::PDF: importPdf(data); break; + case ContentTypeProber::IataBcbp: + importIataBcbp(data); + break; } } void ApplicationController::importPdf(const QByteArray &data) { auto doc = std::unique_ptr(PdfDocument::fromData(data)); if (!doc) return; ExtractorEngine engine; engine.setContextDate(QDateTime::currentDateTime()); engine.setPdfDocument(doc.get()); ExtractorPostprocessor postproc; postproc.setContextDate(QDateTime::currentDateTime()); postproc.process(JsonLdDocument::fromJson(engine.extract())); const auto res = postproc.result(); for (const auto &r : res) { m_resMgr->addReservation(r); } } +void ApplicationController::importIataBcbp(const QByteArray &data) +{ + const auto content = IataBcbpParser::parse(QString::fromUtf8(data), QDate::currentDate()); + ExtractorPostprocessor postproc; + postproc.setContextDate(QDateTime::currentDateTime()); + postproc.process(content); + for (const auto &res : postproc.result()) + m_resMgr->addReservation(res); +} + void ApplicationController::checkCalendar() { #ifdef Q_OS_ANDROID const auto activity = QtAndroid::androidActivity(); if (activity.isValid()) { activity.callMethod("checkCalendar"); } #endif } bool ApplicationController::hasClipboardContent() const { return QGuiApplication::clipboard()->mimeData()->hasText() || QGuiApplication::clipboard()->mimeData()->hasUrls(); } diff --git a/src/app/applicationcontroller.h b/src/app/applicationcontroller.h index bb94f72..070de14 100644 --- a/src/app/applicationcontroller.h +++ b/src/app/applicationcontroller.h @@ -1,96 +1,97 @@ /* 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 QNetworkAccessManager; class ApplicationController : public QObject { Q_OBJECT Q_PROPERTY(bool hasClipboardContent READ hasClipboardContent NOTIFY clipboardContentChanged) public: explicit ApplicationController(QObject *parent = nullptr); ~ApplicationController(); void setReservationManager(ReservationManager *resMgr); void setPkPassManager(PkPassManager *pkPassMgr); // navigation Q_INVOKABLE void showOnMap(const QVariant &place); Q_INVOKABLE bool canNavigateTo(const QVariant &place); Q_INVOKABLE void navigateTo(const QVariant &place); // data import Q_INVOKABLE void showImportFileDialog(); Q_INVOKABLE void importFromClipboard(); Q_INVOKABLE void importFromUrl(const QUrl &url); #ifdef Q_OS_ANDROID void importFromIntent(const QAndroidJniObject &intent); #endif void importData(const QByteArray &data); Q_INVOKABLE void checkCalendar(); static ApplicationController* instance(); bool hasClipboardContent() const; signals: void clipboardContentChanged(); private: void importLocalFile(const QUrl &url, bool isTempFile); void importPdf(const QByteArray &data); + void importIataBcbp(const QByteArray &data); static ApplicationController *s_instance; ReservationManager *m_resMgr = nullptr; PkPassManager *m_pkPassMgr = nullptr; QNetworkAccessManager *m_nam = 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/contenttypeprober.cpp b/src/app/contenttypeprober.cpp index 25d6cfa..dc42652 100644 --- a/src/app/contenttypeprober.cpp +++ b/src/app/contenttypeprober.cpp @@ -1,45 +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 . */ #include "contenttypeprober.h" #include #include ContentTypeProber::Type ContentTypeProber::probe(const QByteArray &head, const QUrl &url) { // check content if (head.size() >= 4) { if (head[0] == 'P' && head[1] == 'K' && head[2] == 0x03 && head[3] == 0x04) return PkPass; if (head[0] == '%' && head[1] == 'P' && head[2] == 'D' && head[3] == 'F') return PDF; if (head[0] == '[' || head[0] == '{') return JsonLd; + if (head[0] == 'M' && head[1] >= '1' && head[1] <= '9') + return IataBcbp; } // guess from file extension const auto fn = url.fileName(); if (fn.endsWith(QLatin1String(".pkpass"), Qt::CaseInsensitive)) return PkPass; if (fn.endsWith(QLatin1String(".pdf"), Qt::CaseInsensitive)) return PDF; if (fn.endsWith(QLatin1String(".json"), Qt::CaseInsensitive) || fn.endsWith(QLatin1String(".jsonld"), Qt::CaseInsensitive)) return JsonLd; return Unknown; } diff --git a/src/app/contenttypeprober.h b/src/app/contenttypeprober.h index 44f5972..e57380a 100644 --- a/src/app/contenttypeprober.h +++ b/src/app/contenttypeprober.h @@ -1,38 +1,39 @@ /* 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 CONTENTTYPEPROBER_H #define CONTENTTYPEPROBER_H #include class QByteArray; /** Detect file type by content and/or URL. */ namespace ContentTypeProber { enum Type { Unknown, PkPass, PDF, - JsonLd + JsonLd, + IataBcbp }; Type probe(const QByteArray &head, const QUrl &url = {}); } #endif // CONTENTTYPEPROBER_H