diff --git a/libs/ui/KisWelcomePageWidget.cpp b/libs/ui/KisWelcomePageWidget.cpp index a624ee7808..a02c8496ca 100644 --- a/libs/ui/KisWelcomePageWidget.cpp +++ b/libs/ui/KisWelcomePageWidget.cpp @@ -1,435 +1,479 @@ /* This file is part of the KDE project * Copyright (C) 2018 Scott Petrovic * * This library 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 library 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 Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisWelcomePageWidget.h" #include #include #include #include #include #include #include #include "kis_action_manager.h" #include "kactioncollection.h" #include "kis_action.h" #include "KConfigGroup" #include "KSharedConfig" #include #include #include "kis_icon_utils.h" #include "krita_utils.h" #include "KoStore.h" #include "kis_config.h" #include "KisDocument.h" #include #include #include #include +#include +#include +#include +#include +#include "opengl/kis_opengl.h" + +#ifdef Q_OS_ANDROID +#include + + +QPushButton* KisWelcomePageWidget::donationLink; +QLabel* KisWelcomePageWidget::donationBannerImage; +#endif + KisWelcomePageWidget::KisWelcomePageWidget(QWidget *parent) : QWidget(parent) { setupUi(this); recentDocumentsListView->setDragEnabled(false); recentDocumentsListView->viewport()->setAutoFillBackground(false); recentDocumentsListView->setSpacing(2); // set up URLs that go to web browser manualLink->setTextFormat(Qt::RichText); manualLink->setTextInteractionFlags(Qt::TextBrowserInteraction); manualLink->setOpenExternalLinks(true); gettingStartedLink->setTextFormat(Qt::RichText); gettingStartedLink->setTextInteractionFlags(Qt::TextBrowserInteraction); gettingStartedLink->setOpenExternalLinks(true); supportKritaLink->setTextFormat(Qt::RichText); supportKritaLink->setTextInteractionFlags(Qt::TextBrowserInteraction); supportKritaLink->setOpenExternalLinks(true); userCommunityLink->setTextFormat(Qt::RichText); userCommunityLink->setTextInteractionFlags(Qt::TextBrowserInteraction); userCommunityLink->setOpenExternalLinks(true); kritaWebsiteLink->setTextFormat(Qt::RichText); kritaWebsiteLink->setTextInteractionFlags(Qt::TextBrowserInteraction); kritaWebsiteLink->setOpenExternalLinks(true); sourceCodeLink->setTextFormat(Qt::RichText); sourceCodeLink->setTextInteractionFlags(Qt::TextBrowserInteraction); sourceCodeLink->setOpenExternalLinks(true); poweredByKDELink->setTextFormat(Qt::RichText); poweredByKDELink->setTextInteractionFlags(Qt::TextBrowserInteraction); poweredByKDELink->setOpenExternalLinks(true); kdeIcon->setIconSize(QSize(20, 20)); kdeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("kde")).pixmap(20)); versionNotificationLabel->setTextFormat(Qt::RichText); versionNotificationLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); versionNotificationLabel->setOpenExternalLinks(true); connect(chkShowNews, SIGNAL(toggled(bool)), newsWidget, SLOT(toggleNews(bool))); connect(newsWidget, SIGNAL(newsDataChanged()), this, SLOT(slotUpdateVersionMessage())); #ifdef Q_OS_ANDROID // enabling this widgets crashes the app, so it is better for it to be hidden for now newsWidget->hide(); helpTitleLabel_2->hide(); chkShowNews->hide(); - donationLink = new QLabel(dropFrameBorder); - donationLink->setOpenExternalLinks(true); - donationLink->setTextInteractionFlags(Qt::TextBrowserInteraction); - + donationLink = new QPushButton(dropFrameBorder); + donationLink->setFlat(true); QFont f = font(); f.setPointSize(15); + f.setUnderline(true); donationLink->setFont(f); + connect(donationLink, SIGNAL(clicked(bool)), this, SLOT(slotStartDonationFlow())); + verticalLayout_3->addWidget(donationLink); + verticalLayout_3->setAlignment(donationLink, Qt::AlignTop); verticalLayout_3->setSpacing(20); - QLabel *donationBannerImage = new QLabel(dropFrameBorder); + donationBannerImage = new QLabel(dropFrameBorder); QString bannerPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, "share/krita/donation/banner.png"); donationBannerImage->setPixmap(QPixmap(bannerPath)); verticalLayout_3->addWidget(donationBannerImage); + + jboolean bannerPurchased = QAndroidJniObject::callStaticMethod("org/krita/android/DonationHelper", "isBadgePurchased", "()Z"); + if (bannerPurchased) { + donationLink->hide(); + donationBannerImage->show(); + QAndroidJniObject::callStaticMethod("org/krita/android/DonationHelper", "endConnection", "()V"); + } else { + donationLink->show(); + donationBannerImage->hide(); + } + #endif // configure the News area KisConfig cfg(true); bool m_getNews = cfg.readEntry("FetchNews", false); chkShowNews->setChecked(m_getNews); setAcceptDrops(true); } KisWelcomePageWidget::~KisWelcomePageWidget() { } void KisWelcomePageWidget::setMainWindow(KisMainWindow* mainWin) { if (mainWin) { m_mainWindow = mainWin; // set the shortcut links from actions (only if a shortcut exists) if ( mainWin->viewManager()->actionManager()->actionByName("file_new")->shortcut().toString() != "") { newFileLinkShortcut->setText(QString("(") + mainWin->viewManager()->actionManager()->actionByName("file_new")->shortcut().toString() + QString(")")); } if (mainWin->viewManager()->actionManager()->actionByName("file_open")->shortcut().toString() != "") { openFileShortcut->setText(QString("(") + mainWin->viewManager()->actionManager()->actionByName("file_open")->shortcut().toString() + QString(")")); } connect(recentDocumentsListView, SIGNAL(clicked(QModelIndex)), this, SLOT(recentDocumentClicked(QModelIndex))); // we need the view manager to actually call actions, so don't create the connections // until after the view manager is set connect(newFileLink, SIGNAL(clicked(bool)), this, SLOT(slotNewFileClicked())); connect(openFileLink, SIGNAL(clicked(bool)), this, SLOT(slotOpenFileClicked())); connect(clearRecentFilesLink, SIGNAL(clicked(bool)), mainWin, SLOT(clearRecentFiles())); slotUpdateThemeColors(); // allows RSS news items to apply analytics tracking. newsWidget->setAnalyticsTracking("?" + analyticsString); } } void KisWelcomePageWidget::showDropAreaIndicator(bool show) { if (!show) { QString dropFrameStyle = "QFrame#dropAreaIndicator { border: 0px }"; dropFrameBorder->setStyleSheet(dropFrameStyle); } else { QColor textColor = qApp->palette().color(QPalette::Text); QColor backgroundColor = qApp->palette().color(QPalette::Background); QColor blendedColor = KritaUtils::blendColors(textColor, backgroundColor, 0.8); // QColor.name() turns it into a hex/web format QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 2px dotted ").append(blendedColor.name()).append(" }") ; dropFrameBorder->setStyleSheet(dropFrameStyle); } } void KisWelcomePageWidget::slotUpdateThemeColors() { textColor = qApp->palette().color(QPalette::Text); backgroundColor = qApp->palette().color(QPalette::Background); // make the welcome screen labels a subtle color so it doesn't clash with the main UI elements blendedColor = KritaUtils::blendColors(textColor, backgroundColor, 0.8); blendedStyle = QString("color: ").append(blendedColor.name()); // what labels to change the color... startTitleLabel->setStyleSheet(blendedStyle); recentDocumentsLabel->setStyleSheet(blendedStyle); helpTitleLabel->setStyleSheet(blendedStyle); newFileLinkShortcut->setStyleSheet(blendedStyle); openFileShortcut->setStyleSheet(blendedStyle); clearRecentFilesLink->setStyleSheet(blendedStyle); recentDocumentsListView->setStyleSheet(blendedStyle); newFileLink->setStyleSheet(blendedStyle); openFileLink->setStyleSheet(blendedStyle); // giving the drag area messaging a dotted border QString dottedBorderStyle = QString("border: 2px dotted ").append(blendedColor.name()).append("; color:").append(blendedColor.name()).append( ";"); dragImageHereLabel->setStyleSheet(dottedBorderStyle); // make drop area QFrame have a dotted line dropFrameBorder->setObjectName("dropAreaIndicator"); QString dropFrameStyle = QString("QFrame#dropAreaIndicator { border: 4px dotted ").append(blendedColor.name()).append("}"); dropFrameBorder->setStyleSheet(dropFrameStyle); // only show drop area when we have a document over the empty area showDropAreaIndicator(false); // add icons for new and open settings to make them stand out a bit more openFileLink->setIconSize(QSize(30, 30)); newFileLink->setIconSize(QSize(30, 30)); openFileLink->setIcon(KisIconUtils::loadIcon("document-open")); newFileLink->setIcon(KisIconUtils::loadIcon("document-new")); kdeIcon->setIcon(KisIconUtils::loadIcon(QStringLiteral("kde")).pixmap(20)); // HTML links seem to be a bit more stubborn with theme changes... setting inline styles to help with color change userCommunityLink->setText(QString("") .append(i18n("User Community")).append("")); gettingStartedLink->setText(QString("") .append(i18n("Getting Started")).append("")); manualLink->setText(QString("") .append(i18n("User Manual")).append("")); supportKritaLink->setText(QString("") .append(i18n("Support Krita")).append("")); kritaWebsiteLink->setText(QString("") .append(i18n("Krita Website")).append("")); sourceCodeLink->setText(QString("") .append(i18n("Source Code")).append("")); poweredByKDELink->setText(QString("") .append(i18n("Powered by KDE")).append("")); slotUpdateVersionMessage(); // text set from RSS feed #ifdef Q_OS_ANDROID - donationLink->setText(QString("") - .append(i18n("Krita is free and open source.")).append("
").append(i18n("Support Krita's Development!")).append("
")); + donationLink->setStyleSheet(blendedStyle); + donationLink->setText(QString(i18n("Get your Krita Supporter Badge here!"))); #endif // re-populate recent files since they might have themed icons populateRecentDocuments(); } void KisWelcomePageWidget::populateRecentDocuments() { m_recentFilesModel.clear(); // clear existing data before it gets re-populated // grab recent files data int numRecentFiles = m_mainWindow->recentFilesUrls().length() > 5 ? 5 : m_mainWindow->recentFilesUrls().length(); // grab at most 5 KisFileIconCreator iconCreator; for (int i = 0; i < numRecentFiles; i++ ) { QStandardItem *recentItem = new QStandardItem(1,2); // 1 row, 1 column recentItem->setIcon(KisIconUtils::loadIcon("document-export")); QString recentFileUrlPath = m_mainWindow->recentFilesUrls().at(i).toLocalFile(); QString fileName = QFileInfo(recentFileUrlPath).fileName(); QList brokenUrls; if (m_thumbnailMap.contains(recentFileUrlPath)) { recentItem->setIcon(m_thumbnailMap[recentFileUrlPath]); } else { QIcon icon; bool success = iconCreator.createFileIcon(recentFileUrlPath, icon, devicePixelRatioF()); if (success) { recentItem->setIcon(icon); m_thumbnailMap[recentFileUrlPath] = recentItem->icon(); } else { brokenUrls << m_mainWindow->recentFilesUrls().at(i); } } Q_FOREACH(const QUrl &url, brokenUrls) { m_mainWindow->removeRecentUrl(url); } // set the recent object with the data if (brokenUrls.isEmpty() || brokenUrls.last().toLocalFile() != recentFileUrlPath) { recentItem->setText(fileName); // what to display for the item recentItem->setToolTip(recentFileUrlPath); m_recentFilesModel.appendRow(recentItem); } } // hide clear and Recent files title if there are none bool hasRecentFiles = m_mainWindow->recentFilesUrls().length() > 0; recentDocumentsLabel->setVisible(hasRecentFiles); clearRecentFilesLink->setVisible(hasRecentFiles); recentDocumentsListView->setIconSize(QSize(48, 48)); recentDocumentsListView->setModel(&m_recentFilesModel); } void KisWelcomePageWidget::slotUpdateVersionMessage() { alertIcon->setIcon(KisIconUtils::loadIcon("warning")); alertIcon->setVisible(false); // find out if we need an update...or if this is a development version: // dev builds contain GIT hash in it and the word git // stable versions do not contain this. if (qApp->applicationVersion().contains("git")) { // Development build QString versionLabelText = QString("") .append(i18n("DEV BUILD")).append(""); versionNotificationLabel->setText(versionLabelText); alertIcon->setVisible(true); versionNotificationLabel->setVisible(true); } else if (newsWidget->hasUpdateAvailable()) { // build URL for label QString versionLabelText = QString("versionLink() + "?" + analyticsString + "version-update" + "\">") .append(i18n("New Version Available!")).append(""); versionNotificationLabel->setVisible(true); versionNotificationLabel->setText(versionLabelText); alertIcon->setVisible(true); } else { // no message needed... exit versionNotificationLabel->setVisible(false); return; } if (!blendedStyle.isNull()) { versionNotificationLabel->setStyleSheet(blendedStyle); } } +#ifdef Q_OS_ANDROID +void KisWelcomePageWidget::slotStartDonationFlow() +{ + QAndroidJniObject::callStaticMethod("org/krita/android/DonationHelper", "startBillingFlow", "()V"); +} +#endif + void KisWelcomePageWidget::dragEnterEvent(QDragEnterEvent *event) { //qDebug() << "dragEnterEvent formats" << event->mimeData()->formats() << "urls" << event->mimeData()->urls() << "has images" << event->mimeData()->hasImage(); showDropAreaIndicator(true); if (event->mimeData()->hasUrls() || event->mimeData()->hasFormat("application/x-krita-node") || event->mimeData()->hasFormat("application/x-qt-image")) { event->accept(); } } void KisWelcomePageWidget::dropEvent(QDropEvent *event) { //qDebug() << "KisWelcomePageWidget::dropEvent() formats" << event->mimeData()->formats() << "urls" << event->mimeData()->urls() << "has images" << event->mimeData()->hasImage(); showDropAreaIndicator(false); if (event->mimeData()->hasUrls() && event->mimeData()->urls().size() > 0) { Q_FOREACH (const QUrl &url, event->mimeData()->urls()) { if (url.toLocalFile().endsWith(".bundle")) { bool r = m_mainWindow->installBundle(url.toLocalFile()); if (!r) { qWarning() << "Could not install bundle" << url.toLocalFile(); } } else { m_mainWindow->openDocument(url, KisMainWindow::None); } } } } void KisWelcomePageWidget::dragMoveEvent(QDragMoveEvent *event) { //qDebug() << "dragMoveEvent"; m_mainWindow->dragMoveEvent(event); if (event->mimeData()->hasUrls() || event->mimeData()->hasFormat("application/x-krita-node") || event->mimeData()->hasFormat("application/x-qt-image")) { event->accept(); } } void KisWelcomePageWidget::dragLeaveEvent(QDragLeaveEvent */*event*/) { //qDebug() << "dragLeaveEvent"; showDropAreaIndicator(false); m_mainWindow->dragLeave(); } void KisWelcomePageWidget::recentDocumentClicked(QModelIndex index) { QString fileUrl = index.data(Qt::ToolTipRole).toString(); m_mainWindow->openDocument(QUrl::fromLocalFile(fileUrl), KisMainWindow::None ); } void KisWelcomePageWidget::slotNewFileClicked() { m_mainWindow->slotFileNew(); } void KisWelcomePageWidget::slotOpenFileClicked() { m_mainWindow->slotFileOpen(); } +#ifdef Q_OS_ANDROID +extern "C" JNIEXPORT void JNICALL +Java_org_krita_android_JNIWrappers_donationSuccessful(JNIEnv* /*env*/, + jobject /*obj*/, + jint /*n*/) +{ + KisWelcomePageWidget::donationLink->hide(); + KisWelcomePageWidget::donationBannerImage->show(); +} +#endif diff --git a/libs/ui/KisWelcomePageWidget.h b/libs/ui/KisWelcomePageWidget.h index b41b248eb1..4b192325a8 100644 --- a/libs/ui/KisWelcomePageWidget.h +++ b/libs/ui/KisWelcomePageWidget.h @@ -1,95 +1,101 @@ /* This file is part of the KDE project * Copyright (C) 2018 Scott Petrovic * * This library 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 library 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 Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KISWELCOMEPAGEWIDGET_H #define KISWELCOMEPAGEWIDGET_H #include "kritaui_export.h" #include "KisViewManager.h" #include "KisMainWindow.h" #include #include "ui_KisWelcomePage.h" #include /// A widget for displaying if no documents are open. This will display in the MDI area class KRITAUI_EXPORT KisWelcomePageWidget : public QWidget, public Ui::KisWelcomePage { Q_OBJECT public: explicit KisWelcomePageWidget(QWidget *parent); ~KisWelcomePageWidget() override; void setMainWindow(KisMainWindow* m_mainWindow); public Q_SLOTS: /// if a document is placed over this area, a dotted line will appear as an indicator /// that it is a droppable area. KisMainwindow is what triggers this void showDropAreaIndicator(bool show); void slotUpdateThemeColors(); /// this could be called multiple times. If a recent document doesn't /// have a preview, an icon is used that needs to be updated void populateRecentDocuments(); void slotUpdateVersionMessage(); +#ifdef Q_OS_ANDROID + void slotStartDonationFlow(); +#endif + protected: // QWidget overrides void dragEnterEvent(QDragEnterEvent * event) override; void dropEvent(QDropEvent * event) override; void dragMoveEvent(QDragMoveEvent * event) override; void dragLeaveEvent(QDragLeaveEvent * event) override; private: KisMainWindow *m_mainWindow; QStandardItemModel m_recentFilesModel; QMap m_thumbnailMap; /// help us see how many people are clicking startup screen links /// you can see the results in Matomo (stats.kde.org) /// this will be listed in the "Acquisition" section of Matomo /// just append some text to this to associate it with an event/page const QString analyticsString = "pk_campaign=startup-sceen&pk_kwd="; // keeping track of link colors with theme change QColor textColor; QColor backgroundColor; QColor blendedColor; QString blendedStyle; - - - QLabel* donationLink; +#ifdef Q_OS_ANDROID +public: + static QPushButton* donationLink; + static QLabel* donationBannerImage; +#endif private Q_SLOTS: void slotNewFileClicked(); void slotOpenFileClicked(); void recentDocumentClicked(QModelIndex index); }; #endif // KISWELCOMEPAGEWIDGET_H diff --git a/packaging/android/apk/AndroidManifest.xml b/packaging/android/apk/AndroidManifest.xml index 99cb457b50..c5df5373f1 100644 --- a/packaging/android/apk/AndroidManifest.xml +++ b/packaging/android/apk/AndroidManifest.xml @@ -1,78 +1,79 @@ + diff --git a/packaging/android/apk/build.gradle b/packaging/android/apk/build.gradle index 0f99674298..179a2bb225 100644 --- a/packaging/android/apk/build.gradle +++ b/packaging/android/apk/build.gradle @@ -1,138 +1,143 @@ buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0' } } repositories { google() jcenter() } apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) } ext { abi = System.getenv('ANDROID_ABI') installPrefix = '../krita-android-build' } task configure() { doLast { if (abi == null && !project.hasProperty("abi")) { logger.error('ANDROID_ABI not specified using the default one instead: armeabi-v7a') abi = 'armeabi-v7a' } // if user specified the ABI through environment then overwrite the one in gradle.properties if (System.getenv('ANDROID_ABI') != null) { def f = file('gradle.properties') def lines = f.readLines() f.write("") for (line in lines) { if (line.matches('abi=.+')) continue f.append("$line\n") } f.append("abi=${abi}\n") } def libs = new File(installPrefix, 'lib') if (!libs.exists()) { throw new GradleException('Krita libraires not found, please check if -p=krita-bin finished without errors') } } } // copy libs(plugins) which start with krita*.so and rename // them to start with `lib_` task copyLibs(type: Copy) { from "$installPrefix/lib" into "libs/${abi ?: 'armeabi-v7a'}" rename ('^krita(.*).so$', 'lib_krita$1.so') } /* * androiddeployqt doesn't fully copy the directories. Hidden directories * to be specific. That's why we copy manually and then delete the partial * one it creates */ task copyAssets(type: Copy) { from "$installPrefix/share/" into 'assets/' include '**' } /* * Remove "share" folder in assets. It is copied both manually and by * androiddeployqt(reason in copyAssets task). */ task removeDuplicateAssets(type: Delete) { delete "assets/share" } copyLibs.dependsOn configure 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() 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', 'lib'] } } // This is needed because, gradle by default ignores hidden assets. aaptOptions { ignoreAssetsPattern "!.foajasoie" } lintOptions { abortOnError false } project.ext.versionCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4] def versionMajor = 4 def versionMinor = 3 def versionRelease = 0 defaultConfig { targetSdkVersion 28 minSdkVersion 23 versionCode project.ext.versionCodes[abi] * 1000000 + versionMajor * 10000 + versionMinor * 100 + versionRelease } preBuild.dependsOn(copyLibs) preBuild.dependsOn(copyAssets) preBuild.dependsOn(removeDuplicateAssets) } + +dependencies { + implementation 'com.android.billingclient:billing:2.2.0' +} + diff --git a/packaging/android/apk/res/values/strings.xml b/packaging/android/apk/res/values/strings.xml index c3545fe5f3..482f93ed44 100644 --- a/packaging/android/apk/res/values/strings.xml +++ b/packaging/android/apk/res/values/strings.xml @@ -1,12 +1,14 @@ Click Double Click Swipe Up Swipe Down Swipe Left Swipe Right Circle Clockwise Circle Counter-Clockwise + Something went wrong... + Cancelled diff --git a/packaging/android/apk/src/org/krita/android/DonationHelper.java b/packaging/android/apk/src/org/krita/android/DonationHelper.java new file mode 100644 index 0000000000..7f0944260a --- /dev/null +++ b/packaging/android/apk/src/org/krita/android/DonationHelper.java @@ -0,0 +1,206 @@ +/* + * This file is part of the KDE project + * Copyright (C) 2020 Sharaf Zaman + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.krita.android; + +import android.util.Log; +import android.widget.Toast; + +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchasesUpdatedListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; +import com.android.billingclient.api.SkuDetailsResponseListener; + +import org.krita.R; +import org.qtproject.qt5.android.QtNative; + +import java.util.ArrayList; +import java.util.List; + +public class DonationHelper implements PurchasesUpdatedListener, BillingClientStateListener, SkuDetailsResponseListener { + + private final String LOG_TAG = "krita.DonationHelper"; + + private BillingClient mBillingClient; + private List mSkuDetails; + + private static DonationHelper sInstance; + + private DonationHelper() { + + mBillingClient = BillingClient.newBuilder(QtNative.getContext()) + .setListener(this) + .enablePendingPurchases() + .build(); + mBillingClient.startConnection(this); + } + + public static DonationHelper getInstance() { + if (sInstance == null) { + sInstance = new DonationHelper(); + } + return sInstance; + } + + @Override + public void onBillingSetupFinished(BillingResult billingResult) { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + querySkuDetails(); + } + } + + private void querySkuDetails() { + List skus = new ArrayList<>(); + skus.add("thankyoukiki"); + + SkuDetailsParams params = SkuDetailsParams.newBuilder() + .setType(BillingClient.SkuType.INAPP) + .setSkusList(skus) + .build(); + + mBillingClient.querySkuDetailsAsync(params, this); + } + + @Override + public void onSkuDetailsResponse(BillingResult billingResult, List list) { + if (billingResult == null) { + Log.e(LOG_TAG, "null billingResult"); + return; + } + + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { + if (list != null) { + mSkuDetails = list; + } + } else { + showToast(R.string.something_wrong); + } + } + + @Override + public void onBillingServiceDisconnected() { + + } + + + @Override + public void onPurchasesUpdated(BillingResult billingResult, List purchases) { + if (billingResult == null) { + Log.e(LOG_TAG, "null billingResult"); + return; + } + switch (billingResult.getResponseCode()) { + case BillingClient.BillingResponseCode.OK: + // only one item, for now + for (Purchase purchase: purchases) { + handlePurchase(purchase); + } + break; + + case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: + // this shouldn't happen with our current logic! + Log.w(LOG_TAG, "Item already owned"); + JNIWrappers.donationSuccessful(); + break; + + case BillingClient.BillingResponseCode.DEVELOPER_ERROR: + Log.e(LOG_TAG, "Dev Error: " + billingResult.getDebugMessage()); + break; + + case BillingClient.BillingResponseCode.USER_CANCELED: + showToast(R.string.cancelled); + break; + + default: + showToast(R.string.something_wrong); + } + } + + private void handlePurchase(Purchase purchase) { + if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { + ackPurchase(purchase); + JNIWrappers.donationSuccessful(); + } + } + + private void ackPurchase(Purchase purchase) { + AcknowledgePurchaseParams params = AcknowledgePurchaseParams + .newBuilder() + .setPurchaseToken(purchase.getPurchaseToken()) + .build(); + mBillingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(BillingResult billingResult) { + Log.d(LOG_TAG, "BillingResult: " + billingResult.getResponseCode()); + } + }); + } + + private static void showToast(final int resourceId) { + QtNative.activity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(QtNative.getContext(), resourceId, Toast.LENGTH_LONG).show(); + } + }); + } + + public static void startBillingFlow() { + if (!getInstance().mBillingClient.isReady()) { + getInstance().mBillingClient.startConnection(sInstance); + showToast(R.string.something_wrong); + return; + } + + if (getInstance().mSkuDetails != null) { + // there's only one for nwo + for (SkuDetails detail: getInstance().mSkuDetails) { + BillingFlowParams flowParams = BillingFlowParams.newBuilder() + .setSkuDetails(detail) + .build(); + + getInstance().mBillingClient.launchBillingFlow(QtNative.activity(), flowParams); + } + } + } + + // This method will be called from C++ side, to see if the banner has been purchased. + // We only have one item right now, so this will do. + public static boolean isBadgePurchased() { + Purchase.PurchasesResult purchasesResult = + getInstance().mBillingClient.queryPurchases(BillingClient.SkuType.INAPP); + + if (purchasesResult.getPurchasesList() != null) + return !purchasesResult.getPurchasesList().isEmpty(); + else + return false; + } + + public static void endConnection() { + getInstance().mBillingClient.endConnection(); + sInstance = null; + } +} diff --git a/packaging/android/apk/src/org/krita/android/JNIWrappers.java b/packaging/android/apk/src/org/krita/android/JNIWrappers.java index 7b4e79f4a4..e5e3d2853b 100644 --- a/packaging/android/apk/src/org/krita/android/JNIWrappers.java +++ b/packaging/android/apk/src/org/krita/android/JNIWrappers.java @@ -1,26 +1,27 @@ /* * This file is part of the KDE project * Copyright (C) 2019 Sharaf Zaman * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.krita.android; class JNIWrappers { public static native void saveState(); public static native void exitFullScreen(); + public static native void donationSuccessful(); } diff --git a/packaging/android/apk/src/org/krita/android/MainActivity.java b/packaging/android/apk/src/org/krita/android/MainActivity.java index 22e15b617e..2503d9cf0f 100644 --- a/packaging/android/apk/src/org/krita/android/MainActivity.java +++ b/packaging/android/apk/src/org/krita/android/MainActivity.java @@ -1,79 +1,81 @@ /* * This file is part of the KDE project * Copyright (C) 2019 Sharaf Zaman * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.krita.android; import android.os.Bundle; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.WindowManager; import org.qtproject.qt5.android.QtNative; import org.qtproject.qt5.android.bindings.QtActivity; public class MainActivity extends QtActivity { public boolean isStartup = true; @Override public void onCreate(Bundle savedInstanceState) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); new ConfigsManager().handleAssets(this); + + DonationHelper.getInstance(); } @Override public void onPause() { super.onPause(); // onPause() _is_ called when the app starts. If the native lib // isn't loaded, it crashes. if (!isStartup) { JNIWrappers.saveState(); } else { isStartup = false; } } @Override public boolean onKeyUp(final int keyCode, final KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && getActionBar() != null && !getActionBar().isShowing()) { JNIWrappers.exitFullScreen(); return true; } return super.onKeyUp(keyCode, event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { // We manually pass these events to the QPA Android because, // android doesn't send events of type other than SOURCE_CLASS_POINTER // to the view which was just tapped. So, this view will never get to // QtSurface, because it doesn't claim focus. if (event.isFromSource(InputDevice.SOURCE_TOUCHPAD)) { return QtNative.sendGenericMotionEvent(event, event.getDeviceId()); } return super.onGenericMotionEvent(event); } }