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); +}