32 Commits

Author SHA1 Message Date
2fbb52b708 Actually fix the issue and not only turn off warning 2024-06-27 22:10:05 +02:00
12d2ffd1fe Fix warning with new android compiler 2024-06-27 21:14:44 +02:00
475aedb0bd fix example qmake build not finding QtZeroConf headers
Noticed on Qt 6.5.1 Android build
2024-04-04 21:45:48 -04:00
83df89fabe fix Android Qt 5.15 builds using cmake 2024-04-04 19:42:15 -04:00
9fbb8dcbc1 changes for qt6-Android
- QT += gui
- QT += androidextras ==> only for Qt5
- QAndroidJniEnvironment ==> QJniEnvironment
- QAndroidJniObject ==> QJniObject
- QtAndroid::runOnAndroidThread ==> Qt5
- QNativeInterface::QAndroidApplication::runOnAndroidMainThread ==> Qt6

Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2024-04-04 17:13:17 -04:00
a2b907b95f cmake change avahi to NDS for Android
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2024-04-04 14:39:51 -04:00
38083c6126 cmake: Install using GNUInstallDirs 2023-09-27 19:27:15 -04:00
d466206fb3 use uniform utf-8 encoding 2023-07-06 15:05:44 +02:00
9e8addde36 README.md - add Android NDS note 2023-05-22 16:03:56 -04:00
65e0713bc3 Android: Call nsd.stopServiceDiscovery() when going to sleep
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.

Was an issue on Android 10 on Levono Tab M10 FHD Plus
TB-X606FA_S300554-220630_BMP
2023-05-22 15:48:52 -04:00
4fada0fb2a Example - Add java source dir to test app example for Android NDS
The java source actually needs to be in $$PWD/android/src/ even though
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
2023-05-22 15:41:12 -04:00
58d9ca7e7b AndroidNds - change java to BSD license for simpler app integration. 2023-05-22 11:36:00 -04:00
39e96235ce update readme Apple App Store deployment & added Distribution Requirements 2023-05-22 11:34:18 -04:00
6df17b9760 AndroidNds - add interface parameter to startServicePublish()
This is just to fix compiling on master, interface is not actually used.
2023-05-22 11:34:01 -04:00
da386b2ac3 Android: Call nsd.unregisterService() synchronously when going to sleep
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.
2023-05-22 09:08:15 -04:00
f89c73695e Android: Fix missing or corrupt service name
The publish service name and type are passed to startServicePublish() as
char pointers.  startServicePublish() calls runOnAndroidThread which asks
the java code to run registerService().  If name and type are objects on
the stack, they could get freed  / deleted before the registerService() is
run in the java thread which would cause registerService() to use deleted
objects.  Fix --> make permanent objects for name and type.
2023-05-22 09:08:15 -04:00
61f5676248 also set pointer size to 64bit fixed on the C++ size
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2023-05-22 09:08:15 -04:00
2a7a8fb9d8 Use jlong instead of jint for intptr_t
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2023-05-22 09:08:15 -04:00
966877a9a0 Queue resolver calls
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2023-05-22 09:08:15 -04:00
3f5650388c use proper pointer type
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2023-05-22 09:08:15 -04:00
24debe31b0 Move Android part to use native Android mDNS API
Signed-off-by: Jonathan Bagg <drwho@infidigm.net>
2023-05-22 09:08:15 -04:00
473d8520f9 Avahi Client - fix crashing is avahi daemon is not running. 2023-05-06 08:32:19 -04:00
7b066b1aed Fix building on FreeBSD, including the example
Closes #55
2022-11-01 08:42:06 -04:00
3d7b094b75 project: examples, enable building as standalone
Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 20:20:35 +11:00
668f7358c4 project: cmake: change include path for INSTALL_INTERFACE
This updates the include path used when this project is used/consumed
via find_package(). The include paths no longer need to be prefixed
with 'QtZeroConf', and so now they can be included the same regardless
of whether this project is being used via FetchContent or find_package

Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 20:14:05 +11:00
2318fb1987 project: bump min required cmake version to 3.4
Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 10:30:24 +11:00
81253d92da project: CMake, set SOVERSION for shared libs. Project VERSION
Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 10:30:24 +11:00
d807d5ab37 project: link against avahi-common
Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 10:30:24 +11:00
0889adbc23 project: cmake: allow building with Qt6
Signed-off-by: Marc Reilly <marc@cpdesign.com.au>
2022-11-01 10:30:24 +11:00
c75cd78cff Add optional parameter for binding service to specific interface
Forgot Linux
2022-10-28 13:57:08 -04:00
16b50c700f Add optional parameter for binding service to specific interface 2022-10-28 13:12:56 -04:00
29be12c558 Fix "multiple socket notifiers for same socket"
because the `addressNotifier` of the `Resolver` is deleted after a new
`QSocketNotifier` has been created. That means there are two socket
notifiers for the same socket for a brief moment causing this warning.
2022-10-04 08:05:29 +02:00
14 changed files with 757 additions and 33 deletions

View File

@ -1,7 +1,8 @@
project(QtZeroConf)
cmake_minimum_required(VERSION 2.8.11)
cmake_minimum_required(VERSION 3.4)
project(QtZeroConf VERSION 0.1.0)
find_package(Qt5 COMPONENTS Core Network)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network)
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@ -16,25 +17,30 @@ add_library(QtZeroConf
qzeroconfservice.cpp
)
include(GNUInstallDirs)
if(BUILD_SHARED_LIBS)
target_compile_definitions(QtZeroConf PRIVATE QT_BUILD_ZEROCONF_LIB)
set_target_properties(QtZeroConf PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION 0)
else()
target_compile_definitions(QtZeroConf PUBLIC QZEROCONF_STATIC)
endif()
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
find_library(avahi-client-lib avahi-client REQUIRED)
find_library(avahi-common-lib avahi-common REQUIRED)
find_path(avahi-client-includes avahi-client/client.h REQUIRED)
find_path(avahi-common-includes avahi-common/defs.h REQUIRED)
target_sources(QtZeroConf PRIVATE
avahi-qt/qt-watch_p.h
avahi-qt/qt-watch.cpp
avahiclient.cpp
)
target_include_directories(QtZeroConf PRIVATE ${avahi-client-includes})
target_include_directories(QtZeroConf PRIVATE ${avahi-client-includes} ${avahi-common-includes})
list(APPEND ${PUBLIC_HEADERS}
avahi-qt/qt-watch.h
)
target_link_libraries(QtZeroConf PRIVATE ${avahi-client-lib})
target_link_libraries(QtZeroConf PRIVATE ${avahi-client-lib} ${avahi-common-lib})
endif()
if(APPLE)
@ -48,9 +54,9 @@ endif()
target_include_directories(QtZeroConf PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include>
$<INSTALL_INTERFACE:include/QtZeroConf>
)
target_link_libraries(QtZeroConf PUBLIC Qt5::Core Qt5::Network)
target_link_libraries(QtZeroConf PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
if(WIN32)
target_sources(QtZeroConf PRIVATE
@ -77,8 +83,7 @@ if(WIN32)
target_include_directories(QtZeroConf PRIVATE "${CMAKE_CURRENT_LIST_DIR}/bonjour-sdk")
endif()
if(ANDROID)
if(ANDROID_AVAHI)
set(ACM "${CMAKE_CURRENT_LIST_DIR}/avahi-common")
set(ACR "${CMAKE_CURRENT_LIST_DIR}/avahi-core")
target_sources(QtZeroConf PRIVATE
@ -137,17 +142,29 @@ if(ANDROID)
avahi-qt/qt-watch.h
)
target_compile_definitions(QtZeroConf PRIVATE HAVE_STRLCPY GETTEXT_PACKAGE HAVE_NETLINK)
elseif(ANDROID)
target_sources(QtZeroConf PRIVATE
qzeroconf.h
androidnsd_p.h
androidnsd.cpp
)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Gui)
target_link_libraries(QtZeroConf PUBLIC Qt${QT_VERSION_MAJOR}::Gui)
if (QT_VERSION_MAJOR EQUAL 5)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS AndroidExtras)
target_link_libraries(QtZeroConf PUBLIC Qt${QT_VERSION_MAJOR}::AndroidExtras)
endif ()
endif()
# install
set(INSTALL_CMAKEDIR "lib/cmake/${PROJECT_NAME}" CACHE STRING "Installation directory for cmake config files")
set(INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}" CACHE STRING "Installation directory for cmake config files")
set_target_properties(QtZeroConf PROPERTIES PUBLIC_HEADER
"${PUBLIC_HEADERS}"
)
install(TARGETS QtZeroConf
EXPORT QtZeroConfConfig
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include/QtZeroConf
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
)
export(TARGETS QtZeroConf
FILE ${CMAKE_CURRENT_BINARY_DIR}/QtZeroConfConfig.cmake

217
QZeroConfNsdManager.java Normal file
View File

@ -0,0 +1,217 @@
/**************************************************************************************************
---------------------------------------------------------------------------------------------------
Copyright (C) 2021 Jonathan Bagg
This file is part of QtZeroConf.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of
conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Jonathan Bagg nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
---------------------------------------------------------------------------------------------------
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 java.util.ArrayList;
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(long id, String name, String type, String hostname, String address, int port, Map<String, byte[]> txtRecords);
public static native void onServiceRemovedJNI(long id, String name);
public static native void onBrowserStateChangedJNI(long id, boolean running, boolean error);
public static native void onPublisherStateChangedJNI(long id, boolean running, boolean error);
public static native void onServiceNameChangedJNI(long id, String newName);
private static String TAG = "QZeroConfNsdManager";
private long 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
// There can only be one resolver at a time per application, we'll need to queue the resolving
static private ArrayList<NsdServiceInfo> resolverQueue = new ArrayList<NsdServiceInfo>();
static private NsdServiceInfo pendingResolve = null;
public QZeroConfNsdManager(long 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) {
enqueueResolver(service);
}
@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.w(TAG, "Resolving failed for: " + serviceInfo.getServiceName() + " " + serviceInfo.getServiceType() + ": " + errorCode);
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
enqueueResolver(pendingResolve);
}
pendingResolve = null;
processResolverQueue();
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
QZeroConfNsdManager.onServiceResolvedJNI(id,
serviceInfo.getServiceName(),
serviceInfo.getServiceType(),
serviceInfo.getHost().getHostName(),
serviceInfo.getHost().getHostAddress(),
serviceInfo.getPort(),
serviceInfo.getAttributes()
);
pendingResolve = null;
processResolverQueue();
}
};
}
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);
}
};
}
private void enqueueResolver(NsdServiceInfo serviceInfo) {
resolverQueue.add(serviceInfo);
processResolverQueue();
}
private void processResolverQueue() {
if (resolverQueue.isEmpty()) {
return;
}
if (pendingResolve != null) {
return;
}
pendingResolve = resolverQueue.get(0);
resolverQueue.remove(0);
nsdManager.resolveService(pendingResolve, initializeResolveListener());
}
}

View File

@ -7,7 +7,7 @@ QZeroConf is a Qt wrapper class for ZeroConf libraries across various platforms.
* Android
* iOS
QZeroConf wraps avahi-client on Linux, avahi-core on Android, and dnssd on Mac, iOS and Windows.
QZeroConf wraps avahi-client on Linux, Network Discovery Service (java) on Android, and dnssd on Mac, iOS and Windows.
### Building
@ -37,6 +37,15 @@ The default is `OFF`.
You can also build the included example project by setting `BUILD_EXAMPLE` to `ON`.
The default for this is `OFF`
#### Android
Prior to Android api 30, QtZeroConf used AvaliCore. AvaliCore no longer works >= api 30 as bind() to netlink sockets was disabled in Android. QtZeroConf now uses the Android java Network Discovery Services. NDS is slightly buggy, but more or less gets the job done. A common issue with NDS is that if the app is in sleep mode and a service is removed on another device, the app does not get notified the service was removed when it wakes back up. ANDROID_PACKAGE_SOURCE_DIR must be added to your app's .pro file.
```
ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android
```
QZeroConfNsdManager.java must then be copied or linked to $$PWD/android/src/QZeroConfNsdManager.java Notice the extra src/ ...gradle expects this.
### API
#### Service Publishing
@ -116,13 +125,26 @@ QZeroConf can be used in QML applications
### Build Dependencies
Qt5
Qt5 or Qt6
On Linux, libavahi-client-dev and libavahi-common-dev
### Distribution Requirements
Distributing Software / Apps that use QtZeroConf must follow the requirements of the LGPLv3. Some of my interpretations of the LGPLv3
* LGPLv3 text or a link to the LGPLv3 text must be distributed with the binary. My preferred way to do this is in the App's "About" section.
* An offer of source code with a link to QtZeroConf must be distributed with the binary. My preferred way to do this is in the App's "About" section
* For Android and iOS apps only, instructions on how to re-link or re-package the App or a link to instructions must be distributed with the binary. My preferred way to do this is in the App's "About" section.
All of the above must be shown to the user at least once, separate from the EULA, with a method for the user to acknowledge they have seen it (an ok button). Ideally all of the above is also listed in the description of the App on the App Store.
### Apple App Store deployment
Publishing GPL software in the App Store is a [violation of the GPL](https://news.ycombinator.com/item?id=3488833). If you need to publish an app in the Apple App Store that uses QZeroConf, please contact me for a copy of QZeroConf with a BSD licence.
Publishing closed source Apps that use a LGPLv3 library in the Apple App Store must provide a method for the end user to 1. update the library in the app and 2. run the new version of the app with the updated library. Qt on iOS further complicates this by using static linking. Closed source Apps on iOS using QtZeroConf must provide the apps object files along with clear step by step instructions on how to re-link the app with a new / different version of QtZeroConf (obligation 1). iOS end uses can run the re-linked App on their device by creating a free iOS developer account and use the time limited signing on that account for their device. (obligation 2) I consider this an poor way to meet obligation 2, but as long as Apple has this mechanism, obligation 2 is meet. I will not pursue copyright infringement as long as the individual / organization is meeting obligation 1 and 2 and the Distribution Requirements above.
### iOS device sleep

368
androidnsd.cpp Normal file
View File

@ -0,0 +1,368 @@
/**************************************************************************************************
---------------------------------------------------------------------------------------------------
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;
}

73
androidnsd_p.h Normal file
View File

@ -0,0 +1,73 @@
/**************************************************************************************************
---------------------------------------------------------------------------------------------------
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 <QMap>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#include <QtAndroid>
#include <QtAndroidExtras>
#include <QAndroidJniObject>
#else
#include <QJniObject>
using QAndroidJniObject = QJniObject;
#endif
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 /*thiz*/, jlong id, jstring name, jstring type, jstring hostname, jstring address, jint port, jobject txtRecords);
static void onServiceRemovedJNI(JNIEnv */*env*/, jobject /*this*/, jlong id, jstring name);
static void onBrowserStateChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jlong id, jboolean running, jboolean error);
static void onPublisherStateChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jlong id, jboolean running, jboolean error);
static void onServiceNameChangedJNI(JNIEnv */*env*/, jobject /*thiz*/, jlong id, jstring newName);
QZeroConf *pub;
QAndroidJniObject nsdManager;
bool browserExists = false;
bool publisherExists = false;
QMap<QByteArray, QByteArray> txtRecords;
QString publishName;
QString publishType;
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

@ -33,7 +33,7 @@ AVAHI_C_DECL_BEGIN
* is calculated like this: RFC1034 mandates maximum length of FQDNs
* is 255. The maximum label length is 63. To minimize the number of
* (non-escaped) dots, we comprise our maximum-length domain name of
* four labels <EFBFBD> 63 characters plus three inner dots. Escaping the
* four labels á 63 characters plus three inner dots. Escaping the
* four labels quadruples their length at maximum. An escaped domain
* name has the therefore the maximum length of 63*4*4+3=1011. A
* trailing NUL and perhaps two unnecessary dots leading and trailing

View File

@ -226,16 +226,19 @@ QZeroConf::~QZeroConf()
delete pri;
}
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port)
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
if (pri->group) {
if (!pri->client || pri->group) { // check client is ok (avahi daemon is running) and group is not already configured
emit error(QZeroConf::serviceRegistrationFailed);
return;
}
if (interface <= 0) {
interface = AVAHI_IF_UNSPEC;
}
pri->group = avahi_entry_group_new(pri->client, QZeroConfPrivate::groupCallback, pri);
int ret = avahi_entry_group_add_service_strlst(pri->group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UPDATE, name, type, domain, NULL, port, pri->txt);
int ret = avahi_entry_group_add_service_strlst(pri->group, interface, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UPDATE, name, type, domain, NULL, port, pri->txt);
if (ret < 0) {
avahi_entry_group_free(pri->group);
pri->group = NULL;
@ -292,8 +295,10 @@ void QZeroConf::clearServiceTxtRecords()
void QZeroConf::startBrowser(QString type, QAbstractSocket::NetworkLayerProtocol protocol)
{
if (pri->browser)
if (!pri->client || pri->browser) { // check client is ok (avahi daemon is running) and browser is not already started
emit error(QZeroConf::browserFailed);
return;
}
switch (protocol) {
case QAbstractSocket::IPv4Protocol: pri->aProtocol = AVAHI_PROTO_INET; break;

View File

@ -72,7 +72,7 @@ public:
ref->ready = 1;
if (ref->registerWaiting) {
ref->registerWaiting = 0;
ref->registerService(ref->name.toUtf8(), ref->type.toUtf8(), ref->domain.toUtf8(), ref->port);
ref->registerService(ref->name.toUtf8(), ref->type.toUtf8(), ref->domain.toUtf8(), ref->port, 0);
}
break;
case AVAHI_SERVER_COLLISION:
@ -230,7 +230,7 @@ public:
resolvers.clear();
}
void registerService(const char *name, const char *type, const char *domain, quint16 port)
void registerService(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
qint32 ret;
group = avahi_s_entry_group_new(server, QZeroConfPrivate::groupCallback, this);
@ -239,7 +239,11 @@ public:
return;
}
ret = avahi_server_add_service_strlst(server, group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UPDATE, name, type, domain, NULL, port, txt);
if (interface <= 0) {
interface = AVAHI_IF_UNSPEC;
}
ret = avahi_server_add_service_strlst(server, group, interface, AVAHI_PROTO_UNSPEC, AVAHI_PUBLISH_UPDATE, name, type, domain, NULL, port, txt);
if (ret < 0) {
avahi_s_entry_group_free(group);
group = NULL;
@ -290,14 +294,14 @@ QZeroConf::~QZeroConf()
delete pri;
}
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port)
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
if (pri->group) {
emit error(QZeroConf::serviceRegistrationFailed);
return;
}
if (pri->ready)
pri->registerService(name, type, domain, port);
pri->registerService(name, type, domain, port, interface);
else {
pri->registerWaiting = 1;
pri->name = name;

View File

@ -190,6 +190,8 @@ void DNSSD_API QZeroConfPrivate::resolverCallback(DNSServiceRef, DNSServiceFlags
resolver->cleanUp();
}
else {
// Fix "multiple socket notifiers for same socket" warning
resolver->addressNotifier.clear();
resolver->addressNotifier = QSharedPointer<QSocketNotifier>::create(sockfd, QSocketNotifier::Read);
connect(resolver->addressNotifier.data(), &QSocketNotifier::activated, resolver, &Resolver::addressReady);
}
@ -266,7 +268,7 @@ QZeroConf::~QZeroConf()
delete pri;
}
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port)
void QZeroConf::startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface)
{
DNSServiceErrorType err;
@ -275,7 +277,7 @@ void QZeroConf::startServicePublish(const char *name, const char *type, const ch
return;
}
err = DNSServiceRegister(&pri->dnssRef, 0, 0,
err = DNSServiceRegister(&pri->dnssRef, 0, interface,
name,
type,
domain,

View File

@ -1,7 +1,8 @@
cmake_minimum_required(VERSION 3.4)
project(QtZeroConfExample)
cmake_minimum_required(VERSION 2.8.11)
find_package(Qt5 COMPONENTS Gui Widgets REQUIRED)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network Gui Widgets REQUIRED)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
@ -21,5 +22,5 @@ add_executable(QtZeroConfExample
target_link_libraries(QtZeroConfExample
QtZeroConf
Qt5::Gui Qt5::Widgets
Qt${QT_VERISON_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets
)

View File

@ -0,0 +1 @@
../../../QZeroConfNsdManager.java

View File

@ -5,3 +5,6 @@ SOURCES= main.cpp window.cpp
DEFINES= QZEROCONF_STATIC
include(../qtzeroconf.pri)
INCLUDEPATH+=../
android: ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android

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,16 @@ ubports|android: {
#avahi-core/iface-none.c avahi-core/iface-pfroute.c avahi-core/avahi-reflector.c
}
android: {
lessThan(QT_MAJOR_VERSION, 6) {
QT += androidextras
}
QT += gui
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

@ -50,7 +50,7 @@ public:
};
QZeroConf(QObject *parent = Q_NULLPTR);
~QZeroConf();
void startServicePublish(const char *name, const char *type, const char *domain, quint16 port);
void startServicePublish(const char *name, const char *type, const char *domain, quint16 port, quint32 interface = 0);
void stopServicePublish(void);
bool publishExists(void);
inline void startBrowser(QString type)
@ -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);