Move Android part to use native Android mDNS API

Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
This commit is contained in:
Michael Zanetti
2021-09-10 13:30:30 +02:00
committed by Jonathan Bagg
parent 473d8520f9
commit 24debe31b0
5 changed files with 576 additions and 1 deletions

180
QZeroConfNsdManager.java Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
---------------------------------------------------------------------------------------------------
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<String, byte[]> 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<String, String> txtRecords) {
registrationName = name;
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName(name);
serviceInfo.setServiceType(type);
serviceInfo.setPort(port);
for (Map.Entry<String, String> 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);
}
};
}
}

322
androidnsd.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
---------------------------------------------------------------------------------------------------
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<QZeroConfPrivate*> s_instances;
QZeroConfPrivate::QZeroConfPrivate(QZeroConf *parent)
{
qRegisterMetaType<QHostAddress>();
qRegisterMetaType<TxtRecordMap>("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<int>(this), QtAndroid::androidActivity().object());
if (nsdManager.isValid()) {
jclass objectClass = env->GetObjectClass(nsdManager.object<jobject>());
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<jstring>(),
QAndroidJniObject::fromString(txtRecords.value(key)).object<jstring>());
}
ref.callMethod<void>("registerService", "(Ljava/lang/String;Ljava/lang/String;ILjava/util/Map;)V",
QAndroidJniObject::fromString(QString(name)).object<jstring>(),
QAndroidJniObject::fromString(QString(type)).object<jstring>(),
port,
txtMap.object());
});
}
void QZeroConfPrivate::stopServicePublish()
{
QAndroidJniObject ref(nsdManager);
QtAndroid::runOnAndroidThread([ref]() {
ref.callMethod<void>("unregisterService");
});
}
void QZeroConfPrivate::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol)
{
Q_UNUSED(protocol)
QAndroidJniObject ref(nsdManager);
QtAndroid::runOnAndroidThread([ref, type]() {
ref.callMethod<void>("discoverServices", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(type).object<jstring>());
});
}
void QZeroConfPrivate::stopBrowser()
{
QAndroidJniObject ref(nsdManager);
QtAndroid::runOnAndroidThread([ref]() {
ref.callMethod<void>("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<QByteArray, QByteArray> 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<jint>("size"); i++) {
QAndroidJniObject key = QAndroidJniObject(env->GetObjectArrayElement(txtKeys.object<jobjectArray>(), i));
QAndroidJniObject valueObj = txt.callObjectMethod("get", "(Ljava/lang/Object;)Ljava/lang/Object;", key.object<jstring>());
if (valueObj.isValid()) {
jboolean isCopy;
jbyte* b = env->GetByteArrayElements(valueObj.object<jbyteArray>(), &isCopy);
QByteArray value((char *)b, env->GetArrayLength(valueObj.object<jbyteArray>()));
env->ReleaseByteArrayElements(valueObj.object<jbyteArray>(), b, JNI_ABORT);
txtMap.insert(key.toString().toUtf8(), value);
} else {
txtMap.insert(key.toString().toUtf8(), QByteArray());
}
}
QZeroConfPrivate *ref = reinterpret_cast<QZeroConfPrivate*>(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<QZeroConfPrivate*>(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<QZeroConfPrivate*>(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<QZeroConfPrivate*>(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<QZeroConfPrivate*>(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>("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;
}

65
androidnsd_p.h Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
---------------------------------------------------------------------------------------------------
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 <QtAndroid>
#include <QtAndroidExtras>
#include <QAndroidJniObject>
class QZeroConfPrivate: QObject
{
Q_OBJECT
public:
typedef QMap<QByteArray, QByteArray> 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<QByteArray, QByteArray> 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);
};

View File

@ -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

View File

@ -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);