diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,6 +36,10 @@
find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets)
if (NOT ANDROID)
find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus)
+else ()
+ find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED AndroidExtras)
+ find_package(Java REQUIRED)
+ include(UseJava)
endif()
find_package(Qt5 ${REQUIRED_QT_VERSION} QUIET OPTIONAL_COMPONENTS TextToSpeech)
set_package_properties(Qt5TextToSpeech PROPERTIES
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -34,6 +34,23 @@
)
endif()
+if (ANDROID)
+ list(APPEND knotifications_SRCS notifybyandroid.cpp)
+ # see qtbase/mkspecs/features/java.prf
+ set(CMAKE_JAVA_COMPILE_FLAGS -source 6 -target 6)
+ if (NOT CMAKE_ANDROID_API VERSION_LESS 23)
+ add_jar(knotifications_jar
+ SOURCES
+ org/kde/knotifications/KNotification.java
+ org/kde/knotifications/NotifyByAndroid.java
+ INCLUDE_JARS ${ANDROID_SDK_ROOT}/platforms/android-${CMAKE_ANDROID_API}/android.jar
+ OUTPUT_NAME KF5Notifications
+ )
+ else()
+ message(WARNING "Android notification backend needs at least API level 23!")
+ endif()
+endif()
+
ecm_qt_declare_logging_category(knotifications_SRCS HEADER debug_p.h IDENTIFIER LOG_KNOTIFICATIONS CATEGORY_NAME org.kde.knotifications)
if (CANBERRA_FOUND)
@@ -123,6 +140,10 @@
target_link_libraries(KF5Notifications PRIVATE dbusmenu-qt5)
endif()
+if (ANDROID)
+ target_link_libraries(KF5Notifications PRIVATE Qt5::AndroidExtras)
+endif()
+
set_target_properties(KF5Notifications PROPERTIES VERSION ${KNOTIFICATIONS_VERSION_STRING}
SOVERSION ${KNOTIFICATIONS_SOVERSION}
EXPORT_NAME Notifications
@@ -191,3 +212,7 @@
ecm_generate_pri_file(BASE_NAME KNotifications LIB_NAME KF5Notifications DEPS "widgets" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KNotifications)
install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR})
+if (ANDROID AND NOT ANDROID_API_LEVEL VERSION_LESS 23)
+ install_jar(knotifications_jar DESTINATION jar)
+ install(FILES KF5Notifications-android-dependencies.xml DESTINATION ${KDE_INSTALL_LIBDIR})
+endif()
diff --git a/src/KF5Notifications-android-dependencies.xml b/src/KF5Notifications-android-dependencies.xml
new file mode 100644
--- /dev/null
+++ b/src/KF5Notifications-android-dependencies.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp
--- a/src/knotificationmanager.cpp
+++ b/src/knotificationmanager.cpp
@@ -43,6 +43,8 @@
#ifndef Q_OS_ANDROID
#include "notifybypopup.h"
#include "notifybyportal.h"
+#else
+#include "notifybyandroid.h"
#endif
#include "debug_p.h"
@@ -145,6 +147,8 @@
} else {
plugin = new NotifyByPopup(this);
}
+#else
+ plugin = new NotifyByAndroid(this);
#endif
addPlugin(plugin);
diff --git a/src/notifybyandroid.h b/src/notifybyandroid.h
new file mode 100644
--- /dev/null
+++ b/src/notifybyandroid.h
@@ -0,0 +1,50 @@
+/*
+ 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 NOTIFYBYANDROID_H
+#define NOTIFYBYANDROID_H
+
+#include "knotificationplugin.h"
+
+#include
+#include
+
+/** Android notification backend. */
+class NotifyByAndroid : public KNotificationPlugin
+{
+ Q_OBJECT
+public:
+ explicit NotifyByAndroid(QObject *parent = nullptr);
+ ~NotifyByAndroid() override;
+
+ // interface of KNotificationPlugin
+ QString optionName() override;
+ void notify(KNotification *notification, KNotifyConfig *config) override;
+ void close(KNotification * notification) override;
+
+ // interface from Java
+ void notificationFinished(int id);
+ void notificationActionInvoked(int id, int action);
+
+private:
+ void notifyDeferred(KNotification *notification, const KNotifyConfig *config);
+
+ QAndroidJniObject m_backend;
+ QHash> m_notifications;
+};
+
+#endif // NOTIFYBYANDROID_H
diff --git a/src/notifybyandroid.cpp b/src/notifybyandroid.cpp
new file mode 100644
--- /dev/null
+++ b/src/notifybyandroid.cpp
@@ -0,0 +1,168 @@
+/*
+ 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 "notifybyandroid.h"
+#include "knotification.h"
+#include "debug_p.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+static NotifyByAndroid *s_instance = nullptr;
+
+static void notificationFinished(JNIEnv *env, jobject that, jint notificationId)
+{
+ Q_UNUSED(env);
+ Q_UNUSED(that);
+ if (s_instance) {
+ s_instance->notificationFinished(notificationId);
+ }
+}
+
+static void notificationActionInvoked(JNIEnv *env, jobject that, jint id, jint action)
+{
+ Q_UNUSED(env);
+ Q_UNUSED(that);
+ if (s_instance) {
+ s_instance->notificationActionInvoked(id, action);
+ }
+}
+
+static const JNINativeMethod methods[] = {
+ {"notificationFinished", "(I)V", (void*)notificationFinished},
+ {"notificationActionInvoked", "(II)V", (void*)notificationActionInvoked}
+};
+
+KNOTIFICATIONS_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void*)
+{
+ static bool initialized = false;
+ if (initialized) {
+ return JNI_VERSION_1_4;
+ }
+ initialized = true;
+
+ JNIEnv *env = nullptr;
+ if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
+ qCWarning(LOG_KNOTIFICATIONS) << "Failed to get JNI environment.";
+ return -1;
+ }
+ jclass cls = env->FindClass("org/kde/knotifications/NotifyByAndroid");
+ if (env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(JNINativeMethod)) < 0) {
+ qCWarning(LOG_KNOTIFICATIONS) << "Failed to register native functions.";
+ return -1;
+ }
+
+ return JNI_VERSION_1_4;
+}
+
+NotifyByAndroid::NotifyByAndroid(QObject* parent) :
+ KNotificationPlugin(parent)
+{
+ s_instance = this;
+#if __ANDROID_API__ >= 23
+ m_backend = QAndroidJniObject("org/kde/knotifications/NotifyByAndroid", "(Landroid/content/Context;)V", QtAndroid::androidContext().object());
+#endif
+}
+
+NotifyByAndroid::~NotifyByAndroid()
+{
+ s_instance = nullptr;
+}
+
+QString NotifyByAndroid::optionName()
+{
+ return QStringLiteral("Popup");
+}
+
+void NotifyByAndroid::notify(KNotification *notification, KNotifyConfig *config)
+{
+ // HACK work around that notification->id() is only populated after returning from here
+ QMetaObject::invokeMethod(this, [this, notification, config](){ notifyDeferred(notification, config); }, Qt::QueuedConnection);
+}
+
+void NotifyByAndroid::notifyDeferred(KNotification* notification, const KNotifyConfig* config)
+{
+ Q_UNUSED(config);
+
+#if __ANDROID_API__ >= 23
+ QAndroidJniEnvironment env;
+
+ QAndroidJniObject n("org/kde/knotifications/KNotification", "()V");
+ n.setField("id", notification->id());
+ n.setField("text", QAndroidJniObject::fromString(notification->text()).object());
+ n.setField("title", QAndroidJniObject::fromString(notification->title()).object());
+
+ // icon
+ QPixmap pixmap;
+ if (!notification->iconName().isEmpty()) {
+ const auto icon = QIcon::fromTheme(notification->iconName());
+ pixmap = icon.pixmap(32, 32);
+ } else {
+ pixmap = notification->pixmap();
+ }
+ QByteArray iconData;
+ QBuffer buffer(&iconData);
+ buffer.open(QIODevice::WriteOnly);
+ pixmap.save(&buffer, "PNG");
+ auto jIconData = env->NewByteArray(iconData.length());
+ env->SetByteArrayRegion(jIconData, 0, iconData.length(), reinterpret_cast(iconData.constData()));
+ n.callMethod("setIconFromData", "([BI)V", jIconData, iconData.length());
+
+ // actions
+ const auto actions = notification->actions();
+ for (const auto &action : actions) {
+ n.callMethod("addAction", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(action).object());
+ }
+
+ m_notifications.insert(notification->id(), notification);
+
+ m_backend.callMethod("notify", "(Lorg/kde/knotifications/KNotification;)V", n.object());
+#else
+ Q_UNUSED(notification);
+#endif
+}
+
+void NotifyByAndroid::close(KNotification* notification)
+{
+#if __ANDROID_API__ >= 23
+ m_backend.callMethod("close", "(I)V", notification->id());
+#endif
+ KNotificationPlugin::close(notification);
+}
+
+void NotifyByAndroid::notificationFinished(int id)
+{
+ qCDebug(LOG_KNOTIFICATIONS) << id;
+ const auto it = m_notifications.find(id);
+ if (it == m_notifications.end()) {
+ return;
+ }
+ m_notifications.erase(it);
+ if (it.value()) {
+ finish(it.value());
+ }
+}
+
+void NotifyByAndroid::notificationActionInvoked(int id, int action)
+{
+ qCDebug(LOG_KNOTIFICATIONS) << id << action;
+ emit actionInvoked(id, action);
+}
diff --git a/src/org/kde/knotifications/KNotification.java b/src/org/kde/knotifications/KNotification.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/knotifications/KNotification.java
@@ -0,0 +1,43 @@
+/*
+ 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 .
+*/
+
+package org.kde.knotifications;
+
+import android.graphics.drawable.Icon;
+import java.util.ArrayList;
+
+/** Java side of KNotification.
+ * Used to convey the relevant notification data to Java.
+ */
+public class KNotification
+{
+ public int id;
+ public String text;
+ public String title;
+ public Icon icon;
+ public ArrayList actions = new ArrayList();
+
+ public void setIconFromData(byte[] data, int length)
+ {
+ icon = Icon.createWithData(data, 0, length);
+ }
+
+ public void addAction(String action)
+ {
+ actions.add(action);
+ }
+}
diff --git a/src/org/kde/knotifications/NotifyByAndroid.java b/src/org/kde/knotifications/NotifyByAndroid.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/knotifications/NotifyByAndroid.java
@@ -0,0 +1,113 @@
+/*
+ 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 .
+*/
+
+package org.kde.knotifications;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+/** Java side of the Android notfication backend. */
+public class NotifyByAndroid extends BroadcastReceiver
+{
+ private static final String TAG = "org.kde.knotifications";
+
+ private static final String NOTIFICATION_ACTION = ".org.kde.knotifications.NOTIFICATION_ACTION";
+ private static final String NOTIFICATION_DELETED = ".org.kde.knotifications.NOTIFICATION_DELETED";
+ private static final String NOTIFICATION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ID";
+ private static final String NOTIFICATION_ACTION_ID_EXTRA = "org.kde.knotifications.NOTIFICATION_ACTION_ID";
+
+ private android.content.Context m_ctx;
+ private NotificationManager m_notificationManager;
+ private int m_uniquePendingIntentId = 0;
+
+ public NotifyByAndroid(android.content.Context context)
+ {
+ Log.i(TAG, context.getPackageName());
+ m_ctx = context;
+ m_notificationManager = (NotificationManager)m_ctx.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(m_ctx.getPackageName() + NOTIFICATION_ACTION);
+ filter.addAction(m_ctx.getPackageName() + NOTIFICATION_DELETED);
+ m_ctx.registerReceiver(this, filter);
+ }
+
+ public void notify(KNotification notification)
+ {
+ Log.i(TAG, notification.text);
+
+ Notification.Builder builder = new Notification.Builder(m_ctx);
+ builder.setSmallIcon(notification.icon);
+ builder.setContentTitle(notification.title);
+ builder.setContentText(notification.text);
+
+ // taping the notification shows the app
+ Intent intent = new Intent(m_ctx, m_ctx.getClass());
+ PendingIntent contentIntent = PendingIntent.getActivity(m_ctx, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ builder.setContentIntent(contentIntent);
+
+ // actions
+ int actionId = 0;
+ for (String actionName : notification.actions) {
+ Intent actionIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_ACTION);
+ actionIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
+ actionIntent.putExtra(NOTIFICATION_ACTION_ID_EXTRA, actionId);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification.Action action = new Notification.Action.Builder(0, actionName, pendingIntent).build();
+ builder.addAction(action);
+ ++actionId;
+ }
+
+ // notfication about user closing the notification
+ Intent deleteIntent = new Intent(m_ctx.getPackageName() + NOTIFICATION_DELETED);
+ deleteIntent.putExtra(NOTIFICATION_ID_EXTRA, notification.id);
+ Log.i(TAG, deleteIntent.getExtras() + " " + notification.id);
+ builder.setDeleteIntent(PendingIntent.getBroadcast(m_ctx, m_uniquePendingIntentId++, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT));
+
+ m_notificationManager.notify(notification.id, builder.build());
+ }
+
+ public void close(int id)
+ {
+ m_notificationManager.cancel(id);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent)
+ {
+ String action = intent.getAction();
+ int id = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1);
+ Log.i(TAG, action + ": " + id + " " + intent.getExtras());
+
+ if (action.equals(m_ctx.getPackageName() + NOTIFICATION_ACTION)) {
+ int actionId = intent.getIntExtra(NOTIFICATION_ACTION_ID_EXTRA, -1);
+ notificationActionInvoked(id, actionId);
+ } else if (action.equals(m_ctx.getPackageName() + NOTIFICATION_DELETED)) {
+ notificationFinished(id);
+ }
+ }
+
+ public native void notificationFinished(int notificationId);
+ public native void notificationActionInvoked(int notificationId, int action);
+}