diff --git a/QZeroConfNsdManager.java b/QZeroConfNsdManager.java new file mode 100644 index 0000000..8164bba --- /dev/null +++ b/QZeroConfNsdManager.java @@ -0,0 +1,180 @@ +/************************************************************************************************** +--------------------------------------------------------------------------------------------------- + Copyright (C) 2015 Jonathan Bagg + This file is part of QtZeroConf. + + QtZeroConf is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QtZeroConf 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with QtZeroConf. If not, see . +--------------------------------------------------------------------------------------------------- + Project name : QtZeroConf + File name : QZeroConfNsdManager.java + Created : 10 Spetember 2021 + Author(s) : Michael Zanetti +--------------------------------------------------------------------------------------------------- + NsdManager wrapper for use on Android devices +--------------------------------------------------------------------------------------------------- +**************************************************************************************************/ + +package qtzeroconf; + +import java.util.Map; + +import android.util.Log; + +import android.content.Context; + +import android.net.nsd.NsdServiceInfo; +import android.net.nsd.NsdManager; + +public class QZeroConfNsdManager { + + public static native void onServiceResolvedJNI(int id, String name, String type, String hostname, String address, int port, Map txtRecords); + public static native void onServiceRemovedJNI(int id, String name); + public static native void onBrowserStateChangedJNI(int id, boolean running, boolean error); + public static native void onPublisherStateChangedJNI(int id, boolean running, boolean error); + public static native void onServiceNameChangedJNI(int id, String newName); + + private static String TAG = "QZeroConfNsdManager"; + private int id; + private Context context; + private NsdManager nsdManager; + private NsdManager.DiscoveryListener discoveryListener; + private NsdManager.RegistrationListener registrationListener; + private String registrationName; // The original service name that was given for registration, it might change on collisions + + public QZeroConfNsdManager(int id, Context context) { + super(); + this.id = id; + this.context = context; + + nsdManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE); + discoveryListener = initializeDiscoveryListener(); + registrationListener = initializeRegistrationListener(); + } + + public void registerService(String name, String type, int port, Map txtRecords) { + registrationName = name; + + NsdServiceInfo serviceInfo = new NsdServiceInfo(); + serviceInfo.setServiceName(name); + serviceInfo.setServiceType(type); + serviceInfo.setPort(port); + for (Map.Entry entry: txtRecords.entrySet()) { + serviceInfo.setAttribute(entry.getKey(), entry.getValue()); + } + + try { + nsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener); + } catch (IllegalArgumentException e) { + Log.w(TAG, "Error registering service: " + e.toString()); + onPublisherStateChangedJNI(id, false, true); + } + } + + public void unregisterService() { + nsdManager.unregisterService(registrationListener); + } + + public void discoverServices(String serviceType) { + nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discoveryListener); + } + + public void stopServiceDiscovery() { + nsdManager.stopServiceDiscovery(discoveryListener); + } + + private NsdManager.DiscoveryListener initializeDiscoveryListener() { + return new NsdManager.DiscoveryListener() { + + @Override + public void onDiscoveryStarted(String regType) { + QZeroConfNsdManager.onBrowserStateChangedJNI(id, true, false); + } + + @Override + public void onServiceFound(NsdServiceInfo service) { + nsdManager.resolveService(service, initializeResolveListener()); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) { + QZeroConfNsdManager.onServiceRemovedJNI(id, serviceInfo.getServiceName()); + } + + @Override + public void onDiscoveryStopped(String serviceType) { + QZeroConfNsdManager.onBrowserStateChangedJNI(id, false, false); + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + QZeroConfNsdManager.onBrowserStateChangedJNI(id, false, true); + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + QZeroConfNsdManager.onBrowserStateChangedJNI(id, false, true); + } + }; + } + + private NsdManager.ResolveListener initializeResolveListener() { + return new NsdManager.ResolveListener() { + + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + Log.d(TAG, "Resolving failed for: " + serviceInfo.getServiceName() + " " + serviceInfo.getServiceType() + ": " + errorCode); + } + + @Override + public void onServiceResolved(NsdServiceInfo serviceInfo) { + QZeroConfNsdManager.onServiceResolvedJNI(id, + serviceInfo.getServiceName(), + serviceInfo.getServiceType(), + serviceInfo.getHost().getHostName(), + serviceInfo.getHost().getHostAddress(), + serviceInfo.getPort(), + serviceInfo.getAttributes() + ); + } + }; + } + + public NsdManager.RegistrationListener initializeRegistrationListener() { + return new NsdManager.RegistrationListener() { + + @Override + public void onServiceRegistered(NsdServiceInfo serviceInfo) { + QZeroConfNsdManager.onPublisherStateChangedJNI(id, true, false); + if (!serviceInfo.getServiceName().equals(registrationName)) { + onServiceNameChangedJNI(id, serviceInfo.getServiceName()); + } + } + + @Override + public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { + QZeroConfNsdManager.onPublisherStateChangedJNI(id, false, true); + } + + @Override + public void onServiceUnregistered(NsdServiceInfo arg0) { + QZeroConfNsdManager.onPublisherStateChangedJNI(id, false, false); + } + + @Override + public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) { + QZeroConfNsdManager.onPublisherStateChangedJNI(id, false, true); + } + }; + } +} diff --git a/androidnsd.cpp b/androidnsd.cpp new file mode 100644 index 0000000..9f24c38 --- /dev/null +++ b/androidnsd.cpp @@ -0,0 +1,322 @@ +/************************************************************************************************** +--------------------------------------------------------------------------------------------------- + Copyright (C) 2015-2021 Jonathan Bagg + This file is part of QtZeroConf. + + QtZeroConf is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QtZeroConf 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with QtZeroConf. If not, see . +--------------------------------------------------------------------------------------------------- + Project name : QtZeroConf + File name : androidnsd.cpp + Created : 10 Spetember 2021 + Author(s) : Michael Zanetti +--------------------------------------------------------------------------------------------------- + NsdManager wrapper for use on Android devices +--------------------------------------------------------------------------------------------------- +**************************************************************************************************/ +#include "androidnsd_p.h" + +Q_DECLARE_METATYPE(QHostAddress) + +static QMutex s_instancesMutex; +static QList s_instances; + + +QZeroConfPrivate::QZeroConfPrivate(QZeroConf *parent) +{ + qRegisterMetaType(); + qRegisterMetaType("TxtRecordMap"); + + pub = parent; + + QAndroidJniEnvironment env; + + JNINativeMethod methods[] { + { "onServiceResolvedJNI", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/util/Map;)V", (void*)QZeroConfPrivate::onServiceResolvedJNI }, + { "onServiceRemovedJNI", "(ILjava/lang/String;)V", (void*)QZeroConfPrivate::onServiceRemovedJNI }, + { "onBrowserStateChangedJNI", "(IZZ)V", (void*)QZeroConfPrivate::onBrowserStateChangedJNI }, + { "onPublisherStateChangedJNI", "(IZZ)V", (void*)QZeroConfPrivate::onPublisherStateChangedJNI }, + { "onServiceNameChangedJNI", "(ILjava/lang/String;)V", (void*)QZeroConfPrivate::onServiceNameChangedJNI } + }; + + // Passing "this" as ID down to Java so we can access "this" in callbacks. + // There seems to be no straight forward way to match the "thiz" pointer from JNI calls to our pointer of the Java class + nsdManager = QAndroidJniObject("qtzeroconf/QZeroConfNsdManager", "(ILandroid/content/Context;)V", reinterpret_cast(this), QtAndroid::androidActivity().object()); + if (nsdManager.isValid()) { + jclass objectClass = env->GetObjectClass(nsdManager.object()); + env->RegisterNatives(objectClass, methods, sizeof(methods) / sizeof(methods[0])); + env->DeleteLocalRef(objectClass); + } + + QMutexLocker locker(&s_instancesMutex); + s_instances.append(this); +} + +QZeroConfPrivate::~QZeroConfPrivate() +{ + QMutexLocker locker(&s_instancesMutex); + s_instances.removeAll(this); +} + +// In order to not having to pay attention to only use thread safe methods on the java side, we're only running +// Java calls on the Android thread. +// To make sure the Java object is not going out of scope and being garbage collected when the QZeroConf object +// is deleted before the worker thread actually starts, keep a new QAndroidJniObject to nsdManager +// which will increase the ref counter in the JVM. +void QZeroConfPrivate::startServicePublish(const char *name, const char *type, quint16 port) +{ + QAndroidJniObject ref(nsdManager); + QtAndroid::runOnAndroidThread([=](){ + QAndroidJniObject txtMap("java/util/HashMap"); + foreach (const QByteArray &key, txtRecords.keys()) { + txtMap.callObjectMethod("put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + QAndroidJniObject::fromString(key).object(), + QAndroidJniObject::fromString(txtRecords.value(key)).object()); + } + + ref.callMethod("registerService", "(Ljava/lang/String;Ljava/lang/String;ILjava/util/Map;)V", + QAndroidJniObject::fromString(QString(name)).object(), + QAndroidJniObject::fromString(QString(type)).object(), + port, + txtMap.object()); + }); +} + +void QZeroConfPrivate::stopServicePublish() +{ + QAndroidJniObject ref(nsdManager); + QtAndroid::runOnAndroidThread([ref]() { + ref.callMethod("unregisterService"); + }); +} + +void QZeroConfPrivate::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol) +{ + Q_UNUSED(protocol) + QAndroidJniObject ref(nsdManager); + QtAndroid::runOnAndroidThread([ref, type]() { + ref.callMethod("discoverServices", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(type).object()); + }); +} + +void QZeroConfPrivate::stopBrowser() +{ + QAndroidJniObject ref(nsdManager); + QtAndroid::runOnAndroidThread([ref]() { + ref.callMethod("stopServiceDiscovery"); + }); +} + +// Callbacks will come in from the android thread. So we're never accessing any of our members directly but instead +// propagate callbacks through Qt::QueuedConnection invokes into the Qt thread. Be sure to check if the instance is still +// alive by checking s_instances while holding the mutex before scheduling the invokation. +void QZeroConfPrivate::onServiceResolvedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jstring name, jstring type, jstring hostname, jstring address, jint port, jobject txtRecords) +{ + QMap txtMap; + QAndroidJniObject txt(txtRecords); + QAndroidJniObject txtKeys = txt.callObjectMethod("keySet", "()Ljava/util/Set;").callObjectMethod("toArray", "()[Ljava/lang/Object;"); + + QAndroidJniEnvironment env; + for (int i = 0; i < txt.callMethod("size"); i++) { + QAndroidJniObject key = QAndroidJniObject(env->GetObjectArrayElement(txtKeys.object(), i)); + QAndroidJniObject valueObj = txt.callObjectMethod("get", "(Ljava/lang/Object;)Ljava/lang/Object;", key.object()); + if (valueObj.isValid()) { + jboolean isCopy; + jbyte* b = env->GetByteArrayElements(valueObj.object(), &isCopy); + QByteArray value((char *)b, env->GetArrayLength(valueObj.object())); + env->ReleaseByteArrayElements(valueObj.object(), b, JNI_ABORT); + txtMap.insert(key.toString().toUtf8(), value); + } else { + txtMap.insert(key.toString().toUtf8(), QByteArray()); + } + } + + QZeroConfPrivate *ref = reinterpret_cast(id); + QMutexLocker locker(&s_instancesMutex); + if (!s_instances.contains(ref)) { + return; + } + QMetaObject::invokeMethod(ref, "onServiceResolved", Qt::QueuedConnection, + Q_ARG(QString, QAndroidJniObject(name).toString()), + Q_ARG(QString, QAndroidJniObject(type).toString()), + Q_ARG(QString, QAndroidJniObject(hostname).toString()), + Q_ARG(QHostAddress, QHostAddress(QAndroidJniObject(address).toString())), + Q_ARG(int, port), + Q_ARG(TxtRecordMap, txtMap) + ); + +} + +void QZeroConfPrivate::onServiceRemovedJNI(JNIEnv */*env*/, jobject /*this*/, jint id, jstring name) +{ + QZeroConfPrivate *ref = reinterpret_cast(id); + QMutexLocker locker(&s_instancesMutex); + if (!s_instances.contains(ref)) { + return; + } + QMetaObject::invokeMethod(ref, "onServiceRemoved", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject(name).toString())); +} + + +void QZeroConfPrivate::onBrowserStateChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jboolean running, jboolean error) +{ + QZeroConfPrivate *ref = reinterpret_cast(id); + QMutexLocker locker(&s_instancesMutex); + if (!s_instances.contains(ref)) { + return; + } + QMetaObject::invokeMethod(ref, "onBrowserStateChanged", Qt::QueuedConnection, Q_ARG(bool, running), Q_ARG(bool, error)); +} + +void QZeroConfPrivate::onPublisherStateChangedJNI(JNIEnv */*env*/, jobject /*this*/, jint id, jboolean running, jboolean error) +{ + QZeroConfPrivate *ref = reinterpret_cast(id); + QMutexLocker locker(&s_instancesMutex); + if (!s_instances.contains(ref)) { + return; + } + QMetaObject::invokeMethod(ref, "onPublisherStateChanged", Qt::QueuedConnection, Q_ARG(bool, running), Q_ARG(bool, error)); +} + +void QZeroConfPrivate::onServiceNameChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jstring newName) +{ + QZeroConfPrivate *ref = reinterpret_cast(id); + QMutexLocker locker(&s_instancesMutex); + if (!s_instances.contains(ref)) { + return; + } + QMetaObject::invokeMethod(ref, "onServiceNameChanged", Qt::QueuedConnection, Q_ARG(QString, QAndroidJniObject(newName).toString())); +} + +void QZeroConfPrivate::onServiceResolved(const QString &name, const QString &type, const QString &hostname, const QHostAddress &address, int port, const TxtRecordMap &txtRecords) +{ + QZeroConfService zcs; + bool newRecord = false; + if (pub->services.contains(name)) { + zcs = pub->services.value(name); + } else { + zcs = QZeroConfService(new QZeroConfServiceData); + newRecord = true; + } + + zcs->m_name = name; + zcs->m_type = type; + // A previous implementation (based on avahi) returned service type as "_http._tcp" but Android API return "._http._tcp" + // Stripping leading dot for backwards compatibility. FIXME: Still not in line with bonjour, which adds a trailing dot. + zcs->m_type.remove(QRegExp("^.")); + zcs->m_host = hostname; + zcs->m_port = port; + zcs->m_ip = address; + zcs->m_txt = txtRecords; + + // Those are not available on Androids NsdManager + // zcs->m_domain = domain; + // zcs->m_interfaceIndex = interface; + + if (newRecord) { + pub->services.insert(name, zcs); + emit pub->serviceAdded(zcs); + } else { + emit pub->serviceUpdated(zcs); + } +} + +void QZeroConfPrivate::onServiceRemoved(const QString &name) +{ + if (pub->services.contains(name)) { + QZeroConfService service = pub->services.take(name); + emit pub->serviceRemoved(service); + } +} + +void QZeroConfPrivate::onBrowserStateChanged(bool running, bool error) +{ + browserExists = running; + if (error) { + emit pub->error(QZeroConf::browserFailed); + } +} + +void QZeroConfPrivate::onPublisherStateChanged(bool running, bool error) +{ + publisherExists = running; + if (running) { + emit pub->servicePublished(); + } + if (error) { + emit pub->error(QZeroConf::serviceRegistrationFailed); + } +} + +void QZeroConfPrivate::onServiceNameChanged(const QString &newName) +{ + emit pub->serviceNameChanged(newName); +} + + +QZeroConf::QZeroConf(QObject *parent) : QObject(parent) +{ + pri = new QZeroConfPrivate(this); + qRegisterMetaType("QZeroConfService"); +} + +QZeroConf::~QZeroConf() +{ + delete pri; +} + +void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port) +{ + Q_UNUSED(domain) // Not supported on Android API + pri->startServicePublish(name, type, port); +} + +void QZeroConf::stopServicePublish(void) +{ + pri->stopServicePublish(); +} + +bool QZeroConf::publishExists(void) +{ + return pri->publisherExists; +} + +void QZeroConf::addServiceTxtRecord(QString nameOnly) +{ + pri->txtRecords.insert(nameOnly.toUtf8(), QByteArray()); +} + +void QZeroConf::addServiceTxtRecord(QString name, QString value) +{ + pri->txtRecords.insert(name.toUtf8(), value.toUtf8()); +} + +void QZeroConf::clearServiceTxtRecords() +{ + pri->txtRecords.clear(); +} + +void QZeroConf::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol) +{ + pri->startBrowser(type, protocol); +} + +void QZeroConf::stopBrowser(void) +{ + pri->stopBrowser(); +} + +bool QZeroConf::browserExists(void) +{ + return pri->browserExists; +} diff --git a/androidnsd_p.h b/androidnsd_p.h new file mode 100644 index 0000000..0e480fb --- /dev/null +++ b/androidnsd_p.h @@ -0,0 +1,65 @@ +/************************************************************************************************** +--------------------------------------------------------------------------------------------------- + Copyright (C) 2015-2021 Jonathan Bagg + This file is part of QtZeroConf. + + QtZeroConf is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + QtZeroConf 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with QtZeroConf. If not, see . +--------------------------------------------------------------------------------------------------- + Project name : QtZeroConf + File name : androidnsd_p.h + Created : 10 Spetember 2021 + Author(s) : Michael Zanetti +--------------------------------------------------------------------------------------------------- + NsdManager wrapper for use on Android devices +--------------------------------------------------------------------------------------------------- +**************************************************************************************************/ +#include "qzeroconf.h" + +#include +#include +#include + +class QZeroConfPrivate: QObject +{ + Q_OBJECT +public: + typedef QMap TxtRecordMap; + + QZeroConfPrivate(QZeroConf *parent); + ~QZeroConfPrivate(); + void startServicePublish(const char *name, const char *type, quint16 port); + void stopServicePublish(); + void startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol); + void stopBrowser(); + static void onServiceResolvedJNI(JNIEnv */*env*/, jobject /*this*/, jint id, jstring name, jstring type, jstring hostname, jstring address, jint port, jobject txtRecords); + static void onServiceRemovedJNI(JNIEnv */*env*/, jobject /*this*/, jint id, jstring name); + static void onBrowserStateChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jboolean running, jboolean error); + static void onPublisherStateChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jboolean running, jboolean error); + static void onServiceNameChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jint id, jstring newName); + + QZeroConf *pub; + QAndroidJniObject nsdManager; + + bool browserExists = false; + bool publisherExists = false; + QMap txtRecords; + + +private slots: + void onServiceResolved(const QString &name, const QString &type, const QString &hostname, const QHostAddress &address, int port, const TxtRecordMap &txtRecords); + void onServiceRemoved(const QString &name); + void onBrowserStateChanged(bool running, bool error); + void onPublisherStateChanged(bool running, bool error); + void onServiceNameChanged(const QString &newName); +}; diff --git a/qtzeroconf.pri b/qtzeroconf.pri index ab841b4..19fc37f 100644 --- a/qtzeroconf.pri +++ b/qtzeroconf.pri @@ -57,7 +57,7 @@ ios { QMAKE_CXXFLAGS+= -I$$PWD } -ubports|android: { +ubports: { QMAKE_CXXFLAGS+= -I$$PWD QMAKE_CFLAGS+= -I$$PWD ACM = $$PWD/avahi-common @@ -121,6 +121,13 @@ ubports|android: { #avahi-core/iface-none.c avahi-core/iface-pfroute.c avahi-core/avahi-reflector.c } +android: { + QT += androidextras + HEADERS += $$PWD/qzeroconf.h $$PWD/androidnsd_p.h + SOURCES += $$PWD/androidnsd.cpp + DISTFILES += $$PWD/QZeroConfNsdManager.java +} + HEADERS+= $$PWD/qzeroconfservice.h $$PWD/qzeroconfglobal.h SOURCES+= $$PWD/qzeroconfservice.cpp diff --git a/qzeroconf.h b/qzeroconf.h index cbbbb0d..d252f29 100644 --- a/qzeroconf.h +++ b/qzeroconf.h @@ -66,6 +66,7 @@ public: Q_SIGNALS: void servicePublished(void); + void serviceNameChanged(const QString &newName); void error(QZeroConf::error_t); void serviceAdded(QZeroConfService); void serviceUpdated(QZeroConfService);