forked from jbagg/QtZeroConf
369 lines
13 KiB
C++
369 lines
13 KiB
C++
/**************************************************************************************************
|
|
---------------------------------------------------------------------------------------------------
|
|
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 <QGuiApplication>
|
|
#include <QRegularExpression>
|
|
#include "androidnsd_p.h"
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
|
using QAndroidJniEnvironment = QJniEnvironment;
|
|
#endif
|
|
|
|
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", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/util/Map;)V", (void*)QZeroConfPrivate::onServiceResolvedJNI },
|
|
{ "onServiceRemovedJNI", "(JLjava/lang/String;)V", (void*)QZeroConfPrivate::onServiceRemovedJNI },
|
|
{ "onBrowserStateChangedJNI", "(JZZ)V", (void*)QZeroConfPrivate::onBrowserStateChangedJNI },
|
|
{ "onPublisherStateChangedJNI", "(JZZ)V", (void*)QZeroConfPrivate::onPublisherStateChangedJNI },
|
|
{ "onServiceNameChangedJNI", "(JLjava/lang/String;)V", (void*)QZeroConfPrivate::onServiceNameChangedJNI }
|
|
};
|
|
|
|
// There seems to be no straight forward way to match the "thiz" pointer from JNI calls to our pointer of the Java class
|
|
// Passing "this" as ID down to Java so we can access "this" in callbacks.
|
|
// Note: needs to be quint64 as uintptr_t might be 32 or 64 bit depending on the system, while Java expects a jlong which is always 64 bit.
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
nsdManager = QAndroidJniObject("qtzeroconf/QZeroConfNsdManager", "(JLandroid/content/Context;)V", reinterpret_cast<quint64>(this), QtAndroid::androidActivity().object());
|
|
#else
|
|
nsdManager = QAndroidJniObject("qtzeroconf/QZeroConfNsdManager", "(JLandroid/content/Context;)V", reinterpret_cast<quint64>(this), QNativeInterface::QAndroidApplication::context()
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
|
|
.object<jobject>()
|
|
#endif
|
|
);
|
|
#endif
|
|
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);
|
|
publishName = name;
|
|
publishType = type;
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QtAndroid::runOnAndroidThread([=](){
|
|
#else
|
|
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
|
|
#endif
|
|
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(publishName).object<jstring>(),
|
|
QAndroidJniObject::fromString(publishType).object<jstring>(),
|
|
port,
|
|
txtMap.object());
|
|
});
|
|
}
|
|
|
|
void QZeroConfPrivate::stopServicePublish()
|
|
{
|
|
QAndroidJniObject ref(nsdManager);
|
|
// If Android is on it's way to suspend when stopServicePublish() is called, we need to call nsd.unregisterService() synchronously
|
|
// to force it to run before the device goes to sleep. If instead it is scheduled to run in the Android thread, it will not run
|
|
// until the device is woken back up.
|
|
if (qGuiApp->applicationState() == Qt::ApplicationSuspended) {
|
|
ref.callMethod<void>("unregisterService");
|
|
} else {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QtAndroid::runOnAndroidThread([ref]() {
|
|
#else
|
|
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([ref]() {
|
|
#endif
|
|
ref.callMethod<void>("unregisterService");
|
|
});
|
|
}
|
|
}
|
|
|
|
void QZeroConfPrivate::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol)
|
|
{
|
|
Q_UNUSED(protocol)
|
|
QAndroidJniObject ref(nsdManager);
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QtAndroid::runOnAndroidThread([ref, type]() {
|
|
#else
|
|
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([ref, type]() {
|
|
#endif
|
|
ref.callMethod<void>("discoverServices", "(Ljava/lang/String;)V", QAndroidJniObject::fromString(type).object<jstring>());
|
|
});
|
|
}
|
|
|
|
void QZeroConfPrivate::stopBrowser()
|
|
{
|
|
QAndroidJniObject ref(nsdManager);
|
|
// If Android is on it's way to suspend when stopBrowser() is called, we need to call nsd.stopServiceDiscovery() synchronously
|
|
// to force it to run before the device goes to sleep.
|
|
if (qGuiApp->applicationState() == Qt::ApplicationSuspended) {
|
|
ref.callMethod<void>("stopServiceDiscovery");
|
|
} else {
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
QtAndroid::runOnAndroidThread([ref]() {
|
|
#else
|
|
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([ref]() {
|
|
#endif
|
|
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*/, jlong 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*/, jlong 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*/, jlong 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*/, jlong 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*/, jlong 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(QRegularExpression("^."));
|
|
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, quint32 interface)
|
|
{
|
|
Q_UNUSED(domain) // Not supported on Android API
|
|
Q_UNUSED(interface) // 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;
|
|
}
|