diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /sd/source/ui/remotecontrol | |
parent | Initial commit. (diff) | |
download | libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
32 files changed, 4144 insertions, 0 deletions
diff --git a/sd/source/ui/remotecontrol/AvahiNetworkService.cxx b/sd/source/ui/remotecontrol/AvahiNetworkService.cxx new file mode 100644 index 000000000..7708e6eb7 --- /dev/null +++ b/sd/source/ui/remotecontrol/AvahiNetworkService.cxx @@ -0,0 +1,209 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <config_dbus.h> + +#include <iostream> +#include <limits> +#include <new> +#include <assert.h> + +#include <avahi-client/client.h> +#include <avahi-client/publish.h> + +#include <avahi-common/alternative.h> +#include <avahi-common/error.h> +#include <avahi-common/thread-watch.h> +#include <comphelper/random.hxx> + +#if ENABLE_DBUS +#include <dbus/dbus.h> +#endif + +#include <sal/log.hxx> + +#include "AvahiNetworkService.hxx" +#include "ZeroconfService.hxx" + +using namespace sd; + +static AvahiClient *client = nullptr; +static AvahiThreadedPoll *threaded_poll = nullptr; +static AvahiEntryGroup *group = nullptr; +static AvahiNetworkService *avahiService = nullptr; + +static bool create_services(AvahiClient *c); + +static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata) { + assert(g == group || group == nullptr); + group = g; + + /* Called whenever the entry group state changes */ + + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED : + /* The entry group has been established successfully */ + SAL_INFO( "sdremote.wifi", "Service '" << avahiService->getName() << "' successfully established." ); + break; + + case AVAHI_ENTRY_GROUP_COLLISION : { + char *n; + + /* A service name collision with a remote service + * happened. Let's pick a new name */ + n = avahi_alternative_service_name(avahiService->getName().c_str()); + avahiService->setName(n); + + SAL_INFO( "sdremote.wifi", "Service name collision, renaming service to '" << avahiService->getName() << "'"); + + /* And recreate the services */ + create_services(avahi_entry_group_get_client(g)); + break; + } + + case AVAHI_ENTRY_GROUP_FAILURE : + + SAL_WARN("sdremote.wifi", "Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); + + /* Some kind of failure happened while we were registering our services */ + avahi_threaded_poll_quit(threaded_poll); + break; + + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + ; + } +} + +static bool create_services(AvahiClient *c) { + assert(c); + + /* If this is the first time we're called, let's create a new + * entry group if necessary */ + if(!client) + return false; + + if (!group) + if (!(group = avahi_entry_group_new(c, entry_group_callback, nullptr))) { + SAL_WARN("sdremote.wifi", "avahi_entry_group_new() failed: " << avahi_strerror(avahi_client_errno(c))); + avahiService->clear(); + return false; + } + + /* If the group is empty (either because it was just created, or + * because it was reset previously, add our entries. */ + + if (avahi_entry_group_is_empty(group)) { + SAL_INFO("sdremote.wifi", "Adding service '" << avahiService->getName() << "'"); + char r[128]; + int nRandom = comphelper::rng::uniform_int_distribution(0, std::numeric_limits<int>::max()); + snprintf(r, sizeof(r), "random=%i", nRandom); + int ret = avahi_entry_group_add_service( + group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, static_cast<AvahiPublishFlags>(0), + avahiService->getName().c_str(), kREG_TYPE, nullptr, nullptr, 1599, "local", r, nullptr + ); + if (ret < 0) { + + if (ret == AVAHI_ERR_COLLISION){ + /* A service name collision with a local service happened. Let's + * pick a new name */ + char *n = avahi_alternative_service_name(avahiService->getName().c_str()); + avahiService->setName(n); + + SAL_WARN("sdremote.wifi", "Service name collision, renaming service to '" << avahiService->getName() << "'"); + + avahi_entry_group_reset(group); + + return create_services(c); + } + + SAL_WARN("sdremote.wifi", "Failed to add _impressremote._tcp service: " << avahi_strerror(ret)); + avahiService->clear(); + return false; + } + + /* Tell the server to register the service */ + if ((ret = avahi_entry_group_commit(group)) < 0) { + SAL_WARN("sdremote.wifi", "Failed to commit entry group: " << avahi_strerror(ret)); + avahiService->clear(); + return false; + } + } + + return true; //Services we're already created +} + +static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata) { + assert(c); + + /* Called whenever the client or server state changes */ + + switch (state) { + case AVAHI_CLIENT_S_RUNNING: + create_services(c); + break; + case AVAHI_CLIENT_FAILURE: + SAL_WARN("sdremote.wifi", "Client failure: " << avahi_strerror(avahi_client_errno(c))); + avahiService->clear(); + break; + case AVAHI_CLIENT_S_COLLISION: + case AVAHI_CLIENT_S_REGISTERING: + if (group) + avahi_entry_group_reset(group); + break; + case AVAHI_CLIENT_CONNECTING: + ; + } +} + +void AvahiNetworkService::setup() { +#if ENABLE_DBUS + // Sure, without ENABLE_DBUS it probably makes no sense to try to use this Avahi stuff either, + // but this is just a stop-gap measure to get this to even compile for now with the probably + // pointless combination of configurable options --enable-avahi --enable-dbus --disable-gui. + + // Avahi internally uses D-Bus, which requires the following in order to be + // thread-safe (and we potentially access D-Bus from different threads in + // different places of the code base): + if (!dbus_threads_init_default()) { + throw std::bad_alloc(); + } +#endif + + int error = 0; + avahiService = this; + if (!(threaded_poll = avahi_threaded_poll_new())) { + SAL_WARN("sdremote.wifi", "avahi_threaded_poll_new '" << avahiService->getName() << "' failed"); + return; + } + + if (!(client = avahi_client_new(avahi_threaded_poll_get(threaded_poll), static_cast<AvahiClientFlags>(0), client_callback, nullptr, &error))) { + SAL_WARN("sdremote.wifi", "avahi_client_new failed"); + return; + } + + if(!create_services(client)) + return; + + /* Finally, start the event loop thread */ + if (avahi_threaded_poll_start(threaded_poll) < 0) { + SAL_WARN("sdremote.wifi", "avahi_threaded_poll_start failed"); + return; + } +} + +void AvahiNetworkService::clear() { + /* Call this when the app shuts down */ + if(threaded_poll) + avahi_threaded_poll_stop(threaded_poll); + if(client) + avahi_client_free(client); + if(threaded_poll) + avahi_threaded_poll_free(threaded_poll); +} diff --git a/sd/source/ui/remotecontrol/AvahiNetworkService.hxx b/sd/source/ui/remotecontrol/AvahiNetworkService.hxx new file mode 100644 index 000000000..374a27a3a --- /dev/null +++ b/sd/source/ui/remotecontrol/AvahiNetworkService.hxx @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <string> +#include "ZeroconfService.hxx" + +namespace sd { + + class AvahiNetworkService : public ZeroconfService + { + public: + AvahiNetworkService(const std::string& aname = "", unsigned int aport = 1599) + : ZeroconfService(aname, aport){} + + void clear() override; + void setup() override; + }; +} diff --git a/sd/source/ui/remotecontrol/BluetoothServer.cxx b/sd/source/ui/remotecontrol/BluetoothServer.cxx new file mode 100644 index 000000000..fc3eeff54 --- /dev/null +++ b/sd/source/ui/remotecontrol/BluetoothServer.cxx @@ -0,0 +1,1521 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "BluetoothServer.hxx" + +#include <iostream> +#include <memory> +#include <new> +#include <string_view> + +#include <sal/log.hxx> + +#ifdef LINUX_BLUETOOTH + #include <glib.h> + #include <dbus/dbus.h> + #include <errno.h> + #include <fcntl.h> + #include <unistd.h> + #include <sys/socket.h> + #include <bluetooth/bluetooth.h> + #include <bluetooth/rfcomm.h> + #include "BluetoothServiceRecord.hxx" + #include "BufferedStreamSocket.hxx" +#endif + +#ifdef _WIN32 + // LO vs WinAPI conflict + #undef WB_LEFT + #undef WB_RIGHT + #include <winsock2.h> + #include <ws2bth.h> + #include "BufferedStreamSocket.hxx" +#endif + +#ifdef MACOSX + #include <iomanip> + #include <osl/conditn.hxx> + #include <premac.h> + #import <CoreFoundation/CoreFoundation.h> + #import <IOBluetooth/IOBluetoothUtilities.h> + #import <IOBluetooth/objc/IOBluetoothSDPUUID.h> + #import <IOBluetooth/objc/IOBluetoothSDPServiceRecord.h> + #include <postmac.h> + #import "OSXBluetooth.h" + #include "OSXBluetoothWrapper.hxx" +#endif + +#include "Communicator.hxx" + +using namespace sd; + +#ifdef LINUX_BLUETOOTH + +namespace { + +struct DBusObject { + OString maBusName; + OString maPath; + OString maInterface; + + DBusObject() { } + DBusObject( const char *pBusName, const char *pPath, const char *pInterface ) + : maBusName( pBusName ), maPath( pPath ), maInterface( pInterface ) { } + + DBusMessage *getMethodCall( const char *pName ) + { + return dbus_message_new_method_call( maBusName.getStr(), maPath.getStr(), + maInterface.getStr(), pName ); + } + std::unique_ptr<DBusObject> cloneForInterface( const char *pInterface ) + { + std::unique_ptr<DBusObject> pObject(new DBusObject()); + + pObject->maBusName = maBusName; + pObject->maPath = maPath; + pObject->maInterface = pInterface; + + return pObject; + } +}; + +} + +static std::unique_ptr<DBusObject> getBluez5Adapter(DBusConnection *pConnection); + +struct sd::BluetoothServer::Impl { + // the glib mainloop running in the thread + GMainContext *mpContext; + DBusConnection *mpConnection; + std::unique_ptr<DBusObject> mpService; + enum class BluezVersion { BLUEZ4, BLUEZ5, UNKNOWN }; + BluezVersion maBluezVersion; + + Impl() + : mpContext( g_main_context_new() ) + , mpConnection( nullptr ) + , maBluezVersion( BluezVersion::UNKNOWN ) + { } + + std::unique_ptr<DBusObject> getAdapter() + { + if (mpService) + { + return mpService->cloneForInterface( "org.bluez.Adapter" ); + } + else if (spServer->mpImpl->maBluezVersion == BluezVersion::BLUEZ5) + { + return getBluez5Adapter(mpConnection); + } + else + { + return nullptr; + } + } +}; + +static DBusConnection * +dbusConnectToNameOnBus() +{ + DBusError aError; + DBusConnection *pConnection; + + dbus_error_init( &aError ); + + pConnection = dbus_bus_get( DBUS_BUS_SYSTEM, &aError ); + if( !pConnection || dbus_error_is_set( &aError )) + { + SAL_WARN( "sdremote.bluetooth", "failed to get dbus system bus: " << aError.message ); + dbus_error_free( &aError ); + return nullptr; + } + + return pConnection; +} + +static DBusMessage * +sendUnrefAndWaitForReply( DBusConnection *pConnection, DBusMessage *pMsg ) +{ + DBusPendingCall *pPending = nullptr; + + if( !pMsg || !dbus_connection_send_with_reply( pConnection, pMsg, &pPending, + -1 /* default timeout */ ) ) + { + SAL_WARN( "sdremote.bluetooth", "Memory allocation failed on message send" ); + dbus_message_unref( pMsg ); + return nullptr; + } + dbus_connection_flush( pConnection ); + dbus_message_unref( pMsg ); + + dbus_pending_call_block( pPending ); // block for reply + + pMsg = dbus_pending_call_steal_reply( pPending ); + if( !pMsg ) + SAL_WARN( "sdremote.bluetooth", "no valid reply / timeout" ); + + dbus_pending_call_unref( pPending ); + return pMsg; +} + +static bool +isBluez5Available(DBusConnection *pConnection) +{ + DBusMessage *pMsg; + + // Simplest ways to check whether we have Bluez 5+ is to check + // that we can obtain adapters using the new interfaces. + // The first two error checks however don't tell us anything as they should + // succeed as long as dbus is working correctly. + pMsg = DBusObject( "org.bluez", "/", "org.freedesktop.DBus.ObjectManager" ).getMethodCall( "GetManagedObjects" ); + if (!pMsg) + { + SAL_INFO("sdremote.bluetooth", "No GetManagedObjects call created"); + return false; + } + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + if (!pMsg) + { + SAL_INFO("sdremote.bluetooth", "No reply received"); + return false; + } + + // If dbus is working correctly and we aren't on bluez 5 this is where we + // should actually get the error. + if (dbus_message_get_error_name( pMsg )) + { + SAL_INFO( "sdremote.bluetooth", "GetManagedObjects call failed with \"" + << dbus_message_get_error_name( pMsg ) + << "\" -- we don't seem to have Bluez 5 available"); + return false; + } + SAL_INFO("sdremote.bluetooth", "GetManagedObjects call seems to have succeeded -- we must be on Bluez 5"); + dbus_message_unref(pMsg); + return true; +} + +static std::unique_ptr<DBusObject> +getBluez5Adapter(DBusConnection *pConnection) +{ + DBusMessage *pMsg; + // This returns a list of objects where we need to find the first + // org.bluez.Adapter1 . + pMsg = DBusObject( "org.bluez", "/", "org.freedesktop.DBus.ObjectManager" ).getMethodCall( "GetManagedObjects" ); + if (!pMsg) + return nullptr; + + const gchar* const pInterfaceType = "org.bluez.Adapter1"; + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + DBusMessageIter aObjectIterator; + if (pMsg && dbus_message_iter_init(pMsg, &aObjectIterator)) + { + if (DBUS_TYPE_ARRAY == dbus_message_iter_get_arg_type(&aObjectIterator)) + { + DBusMessageIter aObject; + dbus_message_iter_recurse(&aObjectIterator, &aObject); + do + { + if (DBUS_TYPE_DICT_ENTRY == dbus_message_iter_get_arg_type(&aObject)) + { + DBusMessageIter aContainerIter; + dbus_message_iter_recurse(&aObject, &aContainerIter); + char *pPath = nullptr; + do + { + if (DBUS_TYPE_OBJECT_PATH == dbus_message_iter_get_arg_type(&aContainerIter)) + { + dbus_message_iter_get_basic(&aContainerIter, &pPath); + SAL_INFO( "sdremote.bluetooth", "Something retrieved: '" + << pPath << "' '"); + } + else if (DBUS_TYPE_ARRAY == dbus_message_iter_get_arg_type(&aContainerIter)) + { + DBusMessageIter aInnerIter; + dbus_message_iter_recurse(&aContainerIter, &aInnerIter); + do + { + if (DBUS_TYPE_DICT_ENTRY == dbus_message_iter_get_arg_type(&aInnerIter)) + { + DBusMessageIter aInnerInnerIter; + dbus_message_iter_recurse(&aInnerIter, &aInnerInnerIter); + do + { + if (DBUS_TYPE_STRING == dbus_message_iter_get_arg_type(&aInnerInnerIter)) + { + char* pMessage; + + dbus_message_iter_get_basic(&aInnerInnerIter, &pMessage); + if (pMessage == std::string_view("org.bluez.Adapter1")) + { + dbus_message_unref(pMsg); + if (pPath) + { + return std::make_unique<DBusObject>( "org.bluez", pPath, pInterfaceType ); + } + assert(false); // We should already have pPath provided for us. + } + } + } + while (dbus_message_iter_next(&aInnerInnerIter)); + } + } + while (dbus_message_iter_next(&aInnerIter)); + } + } + while (dbus_message_iter_next(&aContainerIter)); + } + } + while (dbus_message_iter_next(&aObject)); + } + dbus_message_unref(pMsg); + } + + return nullptr; +} + +static DBusObject * +bluez4GetDefaultService( DBusConnection *pConnection ) +{ + DBusMessage *pMsg; + DBusMessageIter it; + const gchar* const pInterfaceType = "org.bluez.Service"; + + // org.bluez.manager only exists for bluez 4. + // getMethodCall should return NULL if there is any issue e.g. the + // if org.bluez.manager doesn't exist. + pMsg = DBusObject( "org.bluez", "/", "org.bluez.Manager" ).getMethodCall( "DefaultAdapter" ); + + if (!pMsg) + { + SAL_WARN("sdremote.bluetooth", "Couldn't retrieve DBusObject for DefaultAdapter"); + return nullptr; + } + + SAL_INFO("sdremote.bluetooth", "successfully retrieved org.bluez.Manager.DefaultAdapter, attempting to use."); + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + if(!pMsg || !dbus_message_iter_init( pMsg, &it ) ) + { + return nullptr; + } + + // This works for Bluez 4 + if( DBUS_TYPE_OBJECT_PATH == dbus_message_iter_get_arg_type( &it ) ) + { + const char *pObjectPath = nullptr; + dbus_message_iter_get_basic( &it, &pObjectPath ); + SAL_INFO( "sdremote.bluetooth", "DefaultAdapter retrieved: '" + << pObjectPath << "' '" << pInterfaceType << "'" ); + dbus_message_unref( pMsg ); + return new DBusObject( "org.bluez", pObjectPath, pInterfaceType ); + } + // Some form of error, e.g. if we have bluez 5 we get a message that + // this method doesn't exist. + else if ( DBUS_TYPE_STRING == dbus_message_iter_get_arg_type( &it ) ) + { + const char *pMessage = nullptr; + dbus_message_iter_get_basic( &it, &pMessage ); + SAL_INFO( "sdremote.bluetooth", "Error message: '" + << pMessage << "' '" << pInterfaceType << "'" ); + } + else + { + SAL_INFO( "sdremote.bluetooth", "invalid type of reply to DefaultAdapter: '" + << static_cast<char>(dbus_message_iter_get_arg_type( &it )) << "'" ); + } + dbus_message_unref(pMsg); + return nullptr; +} + +static bool +bluez4RegisterServiceRecord( DBusConnection *pConnection, DBusObject *pAdapter, + const char *pServiceRecord ) +{ + DBusMessage *pMsg; + DBusMessageIter it; + + pMsg = pAdapter->getMethodCall( "AddRecord" ); + dbus_message_iter_init_append( pMsg, &it ); + dbus_message_iter_append_basic( &it, DBUS_TYPE_STRING, &pServiceRecord ); + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + if( !pMsg || !dbus_message_iter_init( pMsg, &it ) || + dbus_message_iter_get_arg_type( &it ) != DBUS_TYPE_UINT32 ) + { + SAL_WARN( "sdremote.bluetooth", "SDP registration failed" ); + return false; + } + + // We ignore the uint de-registration handle we get back: + // bluez will clean us up automatically on exit + + return true; +} + +static void +bluezCreateAttachListeningSocket( GMainContext *pContext, GPollFD *pSocketFD ) +{ + int nSocket; + + pSocketFD->fd = -1; + + if( ( nSocket = socket( AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM ) ) < 0 ) + { + SAL_WARN( "sdremote.bluetooth", "failed to open bluetooth socket with error " << nSocket ); + return; + } + + sockaddr_rc aAddr; + // Initialize whole structure. Mainly to appease valgrind, which + // doesn't know about the padding at the end of sockaddr_rc which + // it will dutifully check for definedness. But also the standard + // definition of BDADDR_ANY is unusable in C++ code, so just use + // memset to set aAddr.rc_bdaddr to 0. + memset( &aAddr, 0, sizeof( aAddr ) ); + aAddr.rc_family = AF_BLUETOOTH; + aAddr.rc_channel = 5; + + int a; + if ( ( a = bind( nSocket, reinterpret_cast<sockaddr*>(&aAddr), sizeof(aAddr) ) ) < 0 ) { + SAL_WARN( "sdremote.bluetooth", "bind failed with error" << a ); + close( nSocket ); + return; + } + + if ( ( a = listen( nSocket, 1 ) ) < 0 ) + { + SAL_WARN( "sdremote.bluetooth", "listen failed with error" << a ); + close( nSocket ); + return; + } + + // set non-blocking behaviour ... + if( fcntl( nSocket, F_SETFL, O_NONBLOCK) < 0 ) + { + close( nSocket ); + return; + } + + pSocketFD->fd = nSocket; + pSocketFD->events = G_IO_IN | G_IO_PRI; + pSocketFD->revents = 0; + + g_main_context_add_poll( pContext, pSocketFD, G_PRIORITY_DEFAULT ); +} + +static void +bluezDetachCloseSocket( GMainContext *pContext, GPollFD *pSocketFD ) +{ + if( pSocketFD->fd >= 0 ) + { + close( pSocketFD->fd ); + g_main_context_remove_poll( pContext, pSocketFD ); + pSocketFD->fd = -1; + } +} + +#endif // LINUX_BLUETOOTH + +#if defined(MACOSX) + +OSXBluetoothWrapper::OSXBluetoothWrapper( IOBluetoothRFCOMMChannel* channel ) : + mpChannel(channel), + mnMTU(0), + mHaveBytes(), + mMutex(), + mBuffer() +{ + // silly enough, can't write more than mnMTU bytes at once + mnMTU = [channel getMTU]; + + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::OSXBluetoothWrapper(): mnMTU=" << mnMTU ); +} + +sal_Int32 OSXBluetoothWrapper::readLine( OString& aLine ) +{ + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::readLine()" ); + + while( true ) + { + { + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::readLine: entering mutex" ); + ::osl::MutexGuard aQueueGuard( mMutex ); + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::readLine: entered mutex" ); + +#ifdef SAL_LOG_INFO + // We should have in the sal logging some standard way to + // output char buffers with non-printables escaped. + std::ostringstream s; + if (mBuffer.size() > 0) + { + for (unsigned char *p = reinterpret_cast<unsigned char *>(mBuffer.data()); p != reinterpret_cast<unsigned char *>(mBuffer.data()) + mBuffer.size(); p++) + { + if (*p == '\n') + s << "\\n"; + else if (*p < ' ' || *p >= 0x7F) + s << "\\0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(*p) << std::setfill(' ') << std::setw(1) << std::dec; + else + s << *p; + } + } + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::readLine mBuffer: \"" << s.str() << "\"" ); +#endif + + // got enough bytes to return a line? + std::vector<char>::iterator aIt; + if ( (aIt = find( mBuffer.begin(), mBuffer.end(), '\n' )) + != mBuffer.end() ) + { + sal_uInt64 aLocation = aIt - mBuffer.begin(); + + aLine = OString( &(*mBuffer.begin()), aLocation ); + + mBuffer.erase( mBuffer.begin(), aIt + 1 ); // Also delete the empty line + + // yeps + SAL_INFO( "sdremote.bluetooth", " returning, got \"" << OStringToOUString( aLine, RTL_TEXTENCODING_UTF8 ) << "\"" ); + return aLine.getLength() + 1; + } + + // nope - wait some more (after releasing the mutex) + SAL_INFO( "sdremote.bluetooth", " resetting mHaveBytes" ); + mHaveBytes.reset(); + SAL_INFO( "sdremote.bluetooth", " leaving mutex" ); + } + + SAL_INFO( "sdremote.bluetooth", " waiting for mHaveBytes" ); + mHaveBytes.wait(); + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::readLine: got mHaveBytes" ); + } +} + +sal_Int32 OSXBluetoothWrapper::write( const void* pBuffer, sal_uInt32 n ) +{ + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::write(" << pBuffer << ", " << n << ") mpChannel=" << mpChannel ); + + char const * ptr = static_cast<char const *>(pBuffer); + sal_uInt32 nBytesWritten = 0; + + if (mpChannel == nil) + return 0; + + while( nBytesWritten < n ) + { + int toWrite = n - nBytesWritten; + toWrite = toWrite <= mnMTU ? toWrite : mnMTU; + if ( [mpChannel writeSync:const_cast<char *>(ptr) length:toWrite] != kIOReturnSuccess ) + { + SAL_INFO( "sdremote.bluetooth", " [mpChannel writeSync:" << static_cast<void const *>(ptr) << " length:" << toWrite << "] returned error, total written " << nBytesWritten ); + return nBytesWritten; + } + ptr += toWrite; + nBytesWritten += toWrite; + } + SAL_INFO( "sdremote.bluetooth", " total written " << nBytesWritten ); + return nBytesWritten; +} + +void OSXBluetoothWrapper::appendData(void* pBuffer, size_t len) +{ + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::appendData(" << pBuffer << ", " << len << ")" ); + + if( len ) + { + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::appendData: entering mutex" ); + ::osl::MutexGuard aQueueGuard( mMutex ); + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::appendData: entered mutex" ); + mBuffer.insert(mBuffer.begin()+mBuffer.size(), + static_cast<char*>(pBuffer), static_cast<char *>(pBuffer)+len); + SAL_INFO( "sdremote.bluetooth", " setting mHaveBytes" ); + mHaveBytes.set(); + SAL_INFO( "sdremote.bluetooth", " leaving mutex" ); + } +} + +void OSXBluetoothWrapper::channelClosed() +{ + SAL_INFO( "sdremote.bluetooth", "OSXBluetoothWrapper::channelClosed()" ); + + mpChannel = nil; +} + +void incomingCallback( void *userRefCon, + IOBluetoothUserNotificationRef, + IOBluetoothObjectRef objectRef ) +{ + SAL_INFO( "sdremote.bluetooth", "incomingCallback()" ); + + BluetoothServer* pServer = static_cast<BluetoothServer*>(userRefCon); + + IOBluetoothRFCOMMChannel* channel = [IOBluetoothRFCOMMChannel withRFCOMMChannelRef:reinterpret_cast<IOBluetoothRFCOMMChannelRef>(objectRef)]; + + OSXBluetoothWrapper* socket = new OSXBluetoothWrapper( channel); + Communicator* pCommunicator = new Communicator( std::unique_ptr<IBluetoothSocket>(socket) ); + pServer->addCommunicator( pCommunicator ); + + ChannelDelegate* delegate = [[ChannelDelegate alloc] initWithCommunicatorAndSocket: pCommunicator socket: socket]; + [channel setDelegate: delegate]; + [delegate retain]; + + pCommunicator->launch(); +} + +void BluetoothServer::addCommunicator( Communicator* pCommunicator ) +{ + mpCommunicators->push_back( pCommunicator ); +} + +#endif // MACOSX + +#ifdef LINUX_BLUETOOTH + +extern "C" { + static gboolean ensureDiscoverable_cb(gpointer) + { + BluetoothServer::doEnsureDiscoverable(); + return FALSE; // remove source + } + static gboolean restoreDiscoverable_cb(gpointer) + { + BluetoothServer::doRestoreDiscoverable(); + return FALSE; // remove source + } +} + +/* + * Bluez 4 uses custom methods for setting properties, whereas Bluez 5+ + * implements properties using the generic "org.freedesktop.DBus.Properties" + * interface -- hence we have a specific Bluez 4 function to deal with the + * old style of reading properties. + */ +static bool +getBluez4BooleanProperty( DBusConnection *pConnection, DBusObject *pAdapter, + const char *pPropertyName, bool *pBoolean ) +{ + *pBoolean = false; + + if( !pAdapter ) + return false; + + DBusMessage *pMsg; + pMsg = sendUnrefAndWaitForReply( pConnection, + pAdapter->getMethodCall( "GetProperties" ) ); + + DBusMessageIter it; + if( !pMsg || !dbus_message_iter_init( pMsg, &it ) ) + { + SAL_WARN( "sdremote.bluetooth", "no valid reply / timeout" ); + return false; + } + + if( DBUS_TYPE_ARRAY != dbus_message_iter_get_arg_type( &it ) ) + { + SAL_WARN( "sdremote.bluetooth", "no valid reply / timeout" ); + return false; + } + + DBusMessageIter arrayIt; + dbus_message_iter_recurse( &it, &arrayIt ); + + while( dbus_message_iter_get_arg_type( &arrayIt ) == DBUS_TYPE_DICT_ENTRY ) + { + DBusMessageIter dictIt; + dbus_message_iter_recurse( &arrayIt, &dictIt ); + + const char *pName = nullptr; + if( dbus_message_iter_get_arg_type( &dictIt ) == DBUS_TYPE_STRING ) + { + dbus_message_iter_get_basic( &dictIt, &pName ); + if( pName != nullptr && !strcmp( pName, pPropertyName ) ) + { + SAL_INFO( "sdremote.bluetooth", "hit " << pPropertyName << " property" ); + dbus_message_iter_next( &dictIt ); + dbus_bool_t bBool = false; + + if( dbus_message_iter_get_arg_type( &dictIt ) == DBUS_TYPE_VARIANT ) + { + DBusMessageIter variantIt; + dbus_message_iter_recurse( &dictIt, &variantIt ); + + if( dbus_message_iter_get_arg_type( &variantIt ) == DBUS_TYPE_BOOLEAN ) + { + dbus_message_iter_get_basic( &variantIt, &bBool ); + SAL_INFO( "sdremote.bluetooth", "" << pPropertyName << " is " << bBool ); + *pBoolean = bBool; + return true; + } + else + SAL_WARN( "sdremote.bluetooth", "" << pPropertyName << " type " << + dbus_message_iter_get_arg_type( &variantIt ) ); + } + else + SAL_WARN( "sdremote.bluetooth", "variant type ? " << + dbus_message_iter_get_arg_type( &dictIt ) ); + } + else + { + const char *pStr = pName ? pName : "<null>"; + SAL_INFO( "sdremote.bluetooth", "property '" << pStr << "'" ); + } + } + else + SAL_WARN( "sdremote.bluetooth", "unexpected property key type " + << dbus_message_iter_get_arg_type( &dictIt ) ); + dbus_message_iter_next( &arrayIt ); + } + dbus_message_unref( pMsg ); + + return false; +} + +/* + * This gets an org.freedesktop.DBus.Properties boolean + * (as opposed to the old Bluez 4 custom properties methods as visible above). + */ +static bool +getDBusBooleanProperty( DBusConnection *pConnection, DBusObject *pAdapter, + const char *pPropertyName, bool *pBoolean ) +{ + assert( pAdapter ); + + *pBoolean = false; + bool bRet = false; + + std::unique_ptr< DBusObject > pProperties ( + pAdapter->cloneForInterface( "org.freedesktop.DBus.Properties" ) ); + + DBusMessage *pMsg = pProperties->getMethodCall( "Get" ); + + DBusMessageIter itIn; + dbus_message_iter_init_append( pMsg, &itIn ); + const char* pInterface = "org.bluez.Adapter1"; + dbus_message_iter_append_basic( &itIn, DBUS_TYPE_STRING, &pInterface ); + dbus_message_iter_append_basic( &itIn, DBUS_TYPE_STRING, &pPropertyName ); + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + DBusMessageIter it; + if( !pMsg || !dbus_message_iter_init( pMsg, &it ) ) + { + SAL_WARN( "sdremote.bluetooth", "no valid reply / timeout" ); + return false; + } + + if( DBUS_TYPE_VARIANT != dbus_message_iter_get_arg_type( &it ) ) + { + SAL_WARN( "sdremote.bluetooth", "invalid return type" ); + } + else + { + DBusMessageIter variantIt; + dbus_message_iter_recurse( &it, &variantIt ); + + if( dbus_message_iter_get_arg_type( &variantIt ) == DBUS_TYPE_BOOLEAN ) + { + dbus_bool_t bBool = false; + dbus_message_iter_get_basic( &variantIt, &bBool ); + SAL_INFO( "sdremote.bluetooth", "" << pPropertyName << " is " << bBool ); + *pBoolean = bBool; + bRet = true; + } + else + { + SAL_WARN( "sdremote.bluetooth", "" << pPropertyName << " type " << + dbus_message_iter_get_arg_type( &variantIt ) ); + } + + const char* pError = dbus_message_get_error_name( pMsg ); + if ( pError ) + { + SAL_WARN( "sdremote.bluetooth", + "Get failed for " << pPropertyName << " on " << + pAdapter->maPath << " with error: " << pError ); + } + } + dbus_message_unref( pMsg ); + + return bRet; +} + +static void +setDBusBooleanProperty( DBusConnection *pConnection, DBusObject *pAdapter, + const char *pPropertyName, bool bBoolean ) +{ + assert( pAdapter ); + + std::unique_ptr< DBusObject > pProperties( + pAdapter->cloneForInterface( "org.freedesktop.DBus.Properties" ) ); + + DBusMessage *pMsg = pProperties->getMethodCall( "Set" ); + + DBusMessageIter itIn; + dbus_message_iter_init_append( pMsg, &itIn ); + const char* pInterface = "org.bluez.Adapter1"; + dbus_message_iter_append_basic( &itIn, DBUS_TYPE_STRING, &pInterface ); + dbus_message_iter_append_basic( &itIn, DBUS_TYPE_STRING, &pPropertyName ); + + { + DBusMessageIter varIt; + dbus_message_iter_open_container( &itIn, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, &varIt ); + dbus_bool_t bDBusBoolean = bBoolean; + dbus_message_iter_append_basic( &varIt, DBUS_TYPE_BOOLEAN, &bDBusBoolean ); + dbus_message_iter_close_container( &itIn, &varIt ); + } + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + if( !pMsg ) + { + SAL_WARN( "sdremote.bluetooth", "no valid reply / timeout" ); + } + else + { + const char* pError = dbus_message_get_error_name( pMsg ); + if ( pError ) + { + SAL_WARN( "sdremote.bluetooth", + "Set failed for " << pPropertyName << " on " << + pAdapter->maPath << " with error: " << pError ); + } + dbus_message_unref( pMsg ); + } +} + +static bool +getDiscoverable( DBusConnection *pConnection, DBusObject *pAdapter ) +{ + if (pAdapter->maInterface == "org.bluez.Adapter") // Bluez 4 + { + bool bDiscoverable; + if( getBluez4BooleanProperty(pConnection, pAdapter, "Discoverable", &bDiscoverable ) ) + return bDiscoverable; + } + else if (pAdapter->maInterface == "org.bluez.Adapter1") // Bluez 5 + { + bool bDiscoverable; + if ( getDBusBooleanProperty(pConnection, pAdapter, "Discoverable", &bDiscoverable ) ) + return bDiscoverable; + } + return false; +} + +static void +setDiscoverable( DBusConnection *pConnection, DBusObject *pAdapter, bool bDiscoverable ) +{ + SAL_INFO( "sdremote.bluetooth", "setDiscoverable to " << bDiscoverable ); + + if (pAdapter->maInterface == "org.bluez.Adapter") // Bluez 4 + { + bool bPowered = false; + if( !getBluez4BooleanProperty( pConnection, pAdapter, "Powered", &bPowered ) || !bPowered ) + return; // nothing to do + + DBusMessage *pMsg; + DBusMessageIter it, varIt; + + // set timeout to zero + pMsg = pAdapter->getMethodCall( "SetProperty" ); + dbus_message_iter_init_append( pMsg, &it ); + const char *pTimeoutStr = "DiscoverableTimeout"; + dbus_message_iter_append_basic( &it, DBUS_TYPE_STRING, &pTimeoutStr ); + dbus_message_iter_open_container( &it, DBUS_TYPE_VARIANT, + DBUS_TYPE_UINT32_AS_STRING, &varIt ); + dbus_uint32_t nTimeout = 0; + dbus_message_iter_append_basic( &varIt, DBUS_TYPE_UINT32, &nTimeout ); + dbus_message_iter_close_container( &it, &varIt ); + dbus_connection_send( pConnection, pMsg, nullptr ); // async send - why not ? + dbus_message_unref( pMsg ); + + // set discoverable value + pMsg = pAdapter->getMethodCall( "SetProperty" ); + dbus_message_iter_init_append( pMsg, &it ); + const char *pDiscoverableStr = "Discoverable"; + dbus_message_iter_append_basic( &it, DBUS_TYPE_STRING, &pDiscoverableStr ); + dbus_message_iter_open_container( &it, DBUS_TYPE_VARIANT, + DBUS_TYPE_BOOLEAN_AS_STRING, &varIt ); + dbus_bool_t bValue = bDiscoverable; + dbus_message_iter_append_basic( &varIt, DBUS_TYPE_BOOLEAN, &bValue ); + dbus_message_iter_close_container( &it, &varIt ); // async send - why not ? + dbus_connection_send( pConnection, pMsg, nullptr ); + dbus_message_unref( pMsg ); + } + else if (pAdapter->maInterface == "org.bluez.Adapter1") // Bluez 5 + { + setDBusBooleanProperty(pConnection, pAdapter, "Discoverable", bDiscoverable ); + } +} + +static std::unique_ptr<DBusObject> +registerWithDefaultAdapter( DBusConnection *pConnection ) +{ + std::unique_ptr<DBusObject> pService(bluez4GetDefaultService( pConnection )); + if( pService ) + { + if( !bluez4RegisterServiceRecord( pConnection, pService.get(), + bluetooth_service_record ) ) + { + return nullptr; + } + } + + return pService; +} + +static void ProfileUnregisterFunction +(DBusConnection *, void *) +{ + // We specifically don't need to do anything here. +} + +static DBusHandlerResult ProfileMessageFunction +(DBusConnection *pConnection, DBusMessage *pMessage, void *user_data) +{ + SAL_INFO("sdremote.bluetooth", "ProfileMessageFunction||" << dbus_message_get_interface(pMessage) << "||" << dbus_message_get_member(pMessage)); + + if (dbus_message_get_interface(pMessage) == std::string_view("org.bluez.Profile1")) + { + if (dbus_message_get_member(pMessage) == std::string_view("Release")) + { + return DBUS_HANDLER_RESULT_HANDLED; + } + else if (dbus_message_get_member(pMessage) == std::string_view("NewConnection")) + { + if (!dbus_message_has_signature(pMessage, "oha{sv}")) + { + SAL_WARN("sdremote.bluetooth", "wrong signature for NewConnection"); + } + + DBusMessageIter it; + if (!dbus_message_iter_init(pMessage, &it)) + SAL_WARN( "sdremote.bluetooth", "error init dbus" ); + else + { + char* pPath; + dbus_message_iter_get_basic(&it, &pPath); + SAL_INFO("sdremote.bluetooth", "Adapter path:" << pPath); + + if (!dbus_message_iter_next(&it)) + SAL_WARN("sdremote.bluetooth", "not enough parameters passed"); + + // DBUS_TYPE_UNIX_FD == 'h' -- doesn't exist in older versions + // of dbus (< 1.3?) hence defined manually for now + if ('h' == dbus_message_iter_get_arg_type(&it)) + { + + int nDescriptor; + dbus_message_iter_get_basic(&it, &nDescriptor); + std::vector<Communicator*>* pCommunicators = static_cast<std::vector<Communicator*>*>(user_data); + + // Bluez gives us non-blocking sockets, but our code relies + // on blocking behaviour. + (void)fcntl(nDescriptor, F_SETFL, fcntl(nDescriptor, F_GETFL) & ~O_NONBLOCK); + + SAL_INFO( "sdremote.bluetooth", "connection accepted " << nDescriptor); + Communicator* pCommunicator = new Communicator( std::make_unique<BufferedStreamSocket>( nDescriptor ) ); + pCommunicators->push_back( pCommunicator ); + pCommunicator->launch(); + } + + // For some reason an (empty?) reply is expected. + DBusMessage* pRet = dbus_message_new_method_return(pMessage); + dbus_connection_send(pConnection, pRet, nullptr); + dbus_message_unref(pRet); + + // We could read the remote profile version and features here + // (i.e. they are provided as part of the DBusMessage), + // however for us they are irrelevant (as our protocol handles + // equivalent functionality independently of whether we're on + // bluetooth or normal network connection). + return DBUS_HANDLER_RESULT_HANDLED; + } + } + else if (dbus_message_get_member(pMessage) == std::string_view("RequestDisconnection")) + { + return DBUS_HANDLER_RESULT_HANDLED; + } + } + SAL_WARN("sdremote.bluetooth", "Couldn't handle message correctly."); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + +} + +static void +setupBluez5Profile1(DBusConnection* pConnection, std::vector<Communicator*>* pCommunicators) +{ + bool bErr; + + SAL_INFO("sdremote.bluetooth", "Attempting to register our org.bluez.Profile1"); + static DBusObjectPathVTable aVTable; + aVTable.unregister_function = ProfileUnregisterFunction; + aVTable.message_function = ProfileMessageFunction; + + // dbus_connection_try_register_object_path could be used but only exists for + // dbus >= 1.2 -- we really shouldn't be trying this twice in any case. + // (dbus_connection_try_register_object_path also returns an error with more + // information which could be useful for debugging purposes.) + bErr = !dbus_connection_register_object_path(pConnection, "/org/libreoffice/bluez/profile1", &aVTable, pCommunicators); + + if (bErr) + { + SAL_WARN("sdremote.bluetooth", "Failed to register Bluez 5 Profile1 callback, bluetooth won't work."); + } + + dbus_connection_flush( pConnection ); +} + +static void +unregisterBluez5Profile(DBusConnection* pConnection) +{ + DBusMessage* pMsg = dbus_message_new_method_call("org.bluez", "/org/bluez", + "org.bluez.ProfileManager1", "UnregisterProfile"); + DBusMessageIter it; + dbus_message_iter_init_append(pMsg, &it); + + const char *pPath = "/org/libreoffice/bluez/profile1"; + dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &pPath); + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + if (pMsg) + dbus_message_unref(pMsg); + + dbus_connection_unregister_object_path( pConnection, "/org/libreoffice/bluez/profile1"); + + dbus_connection_flush(pConnection); +} + +static bool +registerBluez5Profile(DBusConnection* pConnection, std::vector<Communicator*>* pCommunicators) +{ + setupBluez5Profile1(pConnection, pCommunicators); + + DBusMessage *pMsg; + DBusMessageIter it; + + pMsg = dbus_message_new_method_call("org.bluez", "/org/bluez", + "org.bluez.ProfileManager1", "RegisterProfile"); + dbus_message_iter_init_append(pMsg, &it); + + const char *pPath = "/org/libreoffice/bluez/profile1"; + dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &pPath); + const char *pUUID = "spp"; // Bluez translates this to 0x1101 for spp + dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &pUUID); + + DBusMessageIter aOptionsIter; + dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &aOptionsIter); + + DBusMessageIter aEntry; + + { + dbus_message_iter_open_container(&aOptionsIter, DBUS_TYPE_DICT_ENTRY, nullptr, &aEntry); + + const char *pString = "Name"; + dbus_message_iter_append_basic(&aEntry, DBUS_TYPE_STRING, &pString); + + const char *pValue = "LibreOffice Impress Remote"; + DBusMessageIter aValue; + dbus_message_iter_open_container(&aEntry, DBUS_TYPE_VARIANT, "s", &aValue); + dbus_message_iter_append_basic(&aValue, DBUS_TYPE_STRING, &pValue); + dbus_message_iter_close_container(&aEntry, &aValue); + dbus_message_iter_close_container(&aOptionsIter, &aEntry); + } + + dbus_message_iter_close_container(&it, &aOptionsIter); + + // Other properties that we could set (but don't, since they appear + // to be useless for us): + // "Service": "0x1101" (not needed, but we used to have it in the manually defined profile). + // "Role": setting this to "server" breaks things, although we think we're a server? + // "Channel": seems to be dealt with automatically (but we used to use 5 in the manual profile). + + bool bSuccess = true; + + pMsg = sendUnrefAndWaitForReply( pConnection, pMsg ); + + DBusError aError; + dbus_error_init(&aError); + if (pMsg && dbus_set_error_from_message( &aError, pMsg )) + { + bSuccess = false; + SAL_WARN("sdremote.bluetooth", + "Failed to register our Profile1 with bluez ProfileManager " + << (aError.message ? aError.message : "<null>")); + } + + dbus_error_free(&aError); + if (pMsg) + dbus_message_unref(pMsg); + + dbus_connection_flush(pConnection); + + return bSuccess; +} + +#endif // LINUX_BLUETOOTH + +BluetoothServer::BluetoothServer( std::vector<Communicator*>* pCommunicators ) + : meWasDiscoverable( UNKNOWN ), + mpCommunicators( pCommunicators ) +{ +#ifdef LINUX_BLUETOOTH + // D-Bus requires the following in order to be thread-safe (and we + // potentially access D-Bus from different threads in different places of + // the code base): + if (!dbus_threads_init_default()) { + throw std::bad_alloc(); + } + + mpImpl.reset(new BluetoothServer::Impl()); +#endif +} + +BluetoothServer::~BluetoothServer() +{ +} + +void BluetoothServer::ensureDiscoverable() +{ +#ifdef LINUX_BLUETOOTH + // Push it all across into our mainloop + if( !spServer ) + return; + GSource *pIdle = g_idle_source_new(); + g_source_set_callback( pIdle, ensureDiscoverable_cb, nullptr, nullptr ); + g_source_set_priority( pIdle, G_PRIORITY_DEFAULT ); + g_source_attach( pIdle, spServer->mpImpl->mpContext ); + g_source_unref( pIdle ); +#endif +} + +void BluetoothServer::restoreDiscoverable() +{ +#ifdef LINUX_BLUETOOTH + // Push it all across into our mainloop + if( !spServer ) + return; + GSource *pIdle = g_idle_source_new(); + g_source_set_callback( pIdle, restoreDiscoverable_cb, nullptr, nullptr ); + g_source_set_priority( pIdle, G_PRIORITY_DEFAULT_IDLE ); + g_source_attach( pIdle, spServer->mpImpl->mpContext ); + g_source_unref( pIdle ); +#endif +} + +void BluetoothServer::doEnsureDiscoverable() +{ +#ifdef LINUX_BLUETOOTH + if (!spServer->mpImpl->mpConnection || + spServer->meWasDiscoverable != UNKNOWN ) + return; + + // Find out if we are discoverable already ... + std::unique_ptr<DBusObject> pAdapter = spServer->mpImpl->getAdapter(); + if( !pAdapter ) + return; + + bool bDiscoverable = getDiscoverable(spServer->mpImpl->mpConnection, pAdapter.get() ); + + spServer->meWasDiscoverable = bDiscoverable ? DISCOVERABLE : NOT_DISCOVERABLE; + if( !bDiscoverable ) + setDiscoverable( spServer->mpImpl->mpConnection, pAdapter.get(), true ); +#endif +} + +void BluetoothServer::doRestoreDiscoverable() +{ + if( spServer->meWasDiscoverable == NOT_DISCOVERABLE ) + { +#ifdef LINUX_BLUETOOTH + std::unique_ptr<DBusObject> pAdapter = spServer->mpImpl->getAdapter(); + if( !pAdapter ) + return; + setDiscoverable( spServer->mpImpl->mpConnection, pAdapter.get(), false ); +#endif + } + spServer->meWasDiscoverable = UNKNOWN; +} + +// We have to have all our clients shut otherwise we can't +// re-bind to the same port number it appears. +void BluetoothServer::cleanupCommunicators() +{ + for (auto& rpCommunicator : *mpCommunicators) + rpCommunicator->forceClose(); + // the hope is that all the threads then terminate cleanly and + // clean themselves up. +} + +void SAL_CALL BluetoothServer::run() +{ + SAL_INFO( "sdremote.bluetooth", "BluetoothServer::run called" ); + osl::Thread::setName("BluetoothServer"); +#ifdef LINUX_BLUETOOTH + DBusConnection *pConnection = dbusConnectToNameOnBus(); + if( !pConnection ) + return; + + // For either implementation we need to poll the dbus fd + int fd = -1; + GPollFD aDBusFD; + if( dbus_connection_get_unix_fd( pConnection, &fd ) && fd >= 0 ) + { + aDBusFD.fd = fd; + aDBusFD.events = G_IO_IN | G_IO_PRI; + g_main_context_add_poll( mpImpl->mpContext, &aDBusFD, G_PRIORITY_DEFAULT ); + } + else + SAL_WARN( "sdremote.bluetooth", "failed to poll for incoming dbus signals" ); + + if (isBluez5Available(pConnection)) + { + SAL_INFO("sdremote.bluetooth", "Using Bluez 5"); + registerBluez5Profile(pConnection, mpCommunicators); + mpImpl->mpConnection = pConnection; + mpImpl->maBluezVersion = Impl::BluezVersion::BLUEZ5; + + // We don't need to listen to adapter changes anymore -- profile + // registration is done globally for the entirety of bluez, so we only + // need adapters when setting discoverability, which can be done + // dynamically without the need to listen for changes. + + // TODO: exit on SD deinit + // Probably best to do that in SdModule::~SdModule? + while (true) + { + aDBusFD.revents = 0; + g_main_context_iteration( mpImpl->mpContext, true ); + if( aDBusFD.revents ) + { + dbus_connection_read_write( pConnection, 0 ); + while (DBUS_DISPATCH_DATA_REMAINS == dbus_connection_get_dispatch_status( pConnection )) + dbus_connection_dispatch( pConnection ); + } + if ((false)) break; + // silence Clang -Wunreachable-code after loop (TODO: proper + // fix?) + } + unregisterBluez5Profile( pConnection ); + g_main_context_unref( mpImpl->mpContext ); + mpImpl->mpConnection = nullptr; + mpImpl->mpContext = nullptr; + return; + } + + // Otherwise we could be on Bluez 4 and continue as usual. + mpImpl->maBluezVersion = Impl::BluezVersion::BLUEZ4; + + // Try to setup the default adapter, otherwise wait for add/remove signal + mpImpl->mpService = registerWithDefaultAdapter( pConnection ); + // listen for connection state and power changes - we need to close + // and re-create our socket code on suspend / resume, enable/disable + DBusError aError; + dbus_error_init( &aError ); + dbus_bus_add_match( pConnection, "type='signal',interface='org.bluez.Manager'", &aError ); + dbus_connection_flush( pConnection ); + + // Try to setup the default adapter, otherwise wait for add/remove signal + mpImpl->mpService = registerWithDefaultAdapter( pConnection ); + + // poll on our bluetooth socket - if we can. + GPollFD aSocketFD; + if( mpImpl->mpService ) + bluezCreateAttachListeningSocket( mpImpl->mpContext, &aSocketFD ); + + mpImpl->mpConnection = pConnection; + + while( true ) + { + aDBusFD.revents = 0; + aSocketFD.revents = 0; + g_main_context_iteration( mpImpl->mpContext, true ); + + SAL_INFO( "sdremote.bluetooth", "main-loop spin " + << aDBusFD.revents << " " << aSocketFD.revents ); + if( aDBusFD.revents ) + { + dbus_connection_read_write( pConnection, 0 ); + DBusMessage *pMsg = dbus_connection_pop_message( pConnection ); + if( pMsg ) + { + if( dbus_message_is_signal( pMsg, "org.bluez.Manager", "AdapterRemoved" ) ) + { + SAL_WARN( "sdremote.bluetooth", "lost adapter - cleaning up sockets" ); + bluezDetachCloseSocket( mpImpl->mpContext, &aSocketFD ); + cleanupCommunicators(); + } + else if( dbus_message_is_signal( pMsg, "org.bluez.Manager", "AdapterAdded" ) || + dbus_message_is_signal( pMsg, "org.bluez.Manager", "DefaultAdapterChanged" ) ) + { + SAL_WARN( "sdremote.bluetooth", "gained adapter - re-generating sockets" ); + bluezDetachCloseSocket( mpImpl->mpContext, &aSocketFD ); + cleanupCommunicators(); + mpImpl->mpService = registerWithDefaultAdapter( pConnection ); + if( mpImpl->mpService ) + bluezCreateAttachListeningSocket( mpImpl->mpContext, &aSocketFD ); + } + else + SAL_INFO( "sdremote.bluetooth", "unknown incoming dbus message, " + " type: " << dbus_message_get_type( pMsg ) + << " path: '" << dbus_message_get_path( pMsg ) + << "' interface: '" << dbus_message_get_interface( pMsg ) + << "' member: '" << dbus_message_get_member( pMsg ) ); + } + dbus_message_unref( pMsg ); + } + + if( aSocketFD.revents ) + { + sockaddr_rc aRemoteAddr; + socklen_t aRemoteAddrLen = sizeof(aRemoteAddr); + + SAL_INFO( "sdremote.bluetooth", "performing accept" ); + int nClient = accept( aSocketFD.fd, reinterpret_cast<sockaddr*>(&aRemoteAddr), &aRemoteAddrLen); + if ( nClient < 0 && errno != EAGAIN ) + { + SAL_WARN( "sdremote.bluetooth", "accept failed with errno " << errno ); + } else { + SAL_INFO( "sdremote.bluetooth", "connection accepted " << nClient ); + Communicator* pCommunicator = new Communicator( std::make_unique<BufferedStreamSocket>( nClient ) ); + mpCommunicators->push_back( pCommunicator ); + pCommunicator->launch(); + } + } + if ((false)) break; + // silence Clang -Wunreachable-code after loop (TODO: proper fix?) + } + + unregisterBluez5Profile( pConnection ); + g_main_context_unref( mpImpl->mpContext ); + mpImpl->mpConnection = nullptr; + mpImpl->mpContext = nullptr; + +#elif defined(_WIN32) + WORD wVersionRequested; + WSADATA wsaData; + + wVersionRequested = MAKEWORD(2, 2); + + if ( WSAStartup(wVersionRequested, &wsaData) ) + { + return; // winsock dll couldn't be loaded + } + + int aSocket = socket( AF_BTH, SOCK_STREAM, BTHPROTO_RFCOMM ); + if ( !aSocket ) + { + WSACleanup(); + return; + } + SOCKADDR_BTH aAddr; + aAddr.addressFamily = AF_BTH; + aAddr.btAddr = 0; + aAddr.serviceClassId = GUID_NULL; + aAddr.port = BT_PORT_ANY; // Select any free socket. + if ( bind( aSocket, reinterpret_cast<SOCKADDR*>(&aAddr), sizeof(aAddr) ) == SOCKET_ERROR ) + { + closesocket( aSocket ); + WSACleanup(); + return; + } + + SOCKADDR_BTH aName; + int aNameSize = sizeof(aName); + getsockname( aSocket, reinterpret_cast<SOCKADDR*>(&aName), &aNameSize ); // Retrieve the local address and port + + CSADDR_INFO aAddrInfo = {}; + aAddrInfo.LocalAddr.lpSockaddr = reinterpret_cast<SOCKADDR*>(&aName); + aAddrInfo.LocalAddr.iSockaddrLength = sizeof( SOCKADDR_BTH ); + aAddrInfo.iSocketType = SOCK_STREAM; + aAddrInfo.iProtocol = BTHPROTO_RFCOMM; + + // To be used for setting a custom UUID once available. +// GUID uuid; +// uuid.Data1 = 0x00001101; +// memset( &uuid, 0x1000 + UUID*2^96, sizeof( GUID ) ); +// uuid.Data2 = 0; +// uuid.Data3 = 0x1000; +// ULONGLONG aData4 = 0x800000805F9B34FB; +// memcpy( uuid.Data4, &aData4, sizeof(uuid.Data4) ); + + WSAQUERYSETW aRecord = {}; + aRecord.dwSize = sizeof(aRecord); + aRecord.lpszServiceInstanceName = const_cast<wchar_t *>( + L"LibreOffice Impress Remote Control"); + aRecord.lpszComment = const_cast<wchar_t *>( + L"Remote control of presentations over bluetooth."); + aRecord.lpServiceClassId = const_cast<LPGUID>(&SerialPortServiceClass_UUID); + aRecord.dwNameSpace = NS_BTH; + aRecord.dwNumberOfCsAddrs = 1; + aRecord.lpcsaBuffer = &aAddrInfo; + if (WSASetServiceW( &aRecord, RNRSERVICE_REGISTER, 0 ) == SOCKET_ERROR) + { + closesocket( aSocket ); + WSACleanup(); + return; + } + + if ( listen( aSocket, 1 ) == SOCKET_ERROR ) + { + closesocket( aSocket ); + WSACleanup(); + return; + } + + SOCKADDR_BTH aRemoteAddr; + int aRemoteAddrLen = sizeof(aRemoteAddr); + while ( true ) + { + SOCKET socket; + if ( (socket = accept(aSocket, reinterpret_cast<sockaddr*>(&aRemoteAddr), &aRemoteAddrLen)) == INVALID_SOCKET ) + { + closesocket( aSocket ); + WSACleanup(); + return; + } else { + Communicator* pCommunicator = new Communicator( std::make_unique<BufferedStreamSocket>( socket) ); + mpCommunicators->push_back( pCommunicator ); + pCommunicator->launch(); + } + } + +#elif defined(MACOSX) + // Build up dictionary at run-time instead of bothering with a + // .plist file, using the Objective-C API + + // Compare to BluetoothServiceRecord.hxx + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + NSDictionary *dict = + [NSDictionary dictionaryWithObjectsAndKeys: + + // Service class ID list + [NSArray arrayWithObject: + [IOBluetoothSDPUUID uuid16: kBluetoothSDPUUID16ServiceClassSerialPort]], + @"0001 - ServiceClassIDList", + + // Protocol descriptor list + [NSArray arrayWithObjects: + [NSArray arrayWithObject: [IOBluetoothSDPUUID uuid16: kBluetoothSDPUUID16L2CAP]], + [NSArray arrayWithObjects: + [IOBluetoothSDPUUID uuid16: kBluetoothL2CAPPSMRFCOMM], + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt: 1], + @"DataElementSize", + [NSNumber numberWithInt: 1], + @"DataElementType", + [NSNumber numberWithInt: 5], // RFCOMM port number, will be replaced if necessary automatically + @"DataElementValue", + nil], + nil], + nil], + @"0004 - Protocol descriptor list", + + // Browse group list + [NSArray arrayWithObject: + [IOBluetoothSDPUUID uuid16: kBluetoothSDPUUID16ServiceClassPublicBrowseGroup]], + @"0005 - BrowseGroupList", + + // Language base attribute ID list + [NSArray arrayWithObjects: + [NSData dataWithBytes: "en" length: 2], + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt: 2], + @"DataElementSize", + [NSNumber numberWithInt: 1], + @"DataElementType", + [NSNumber numberWithInt: 0x006a], // encoding + @"DataElementValue", + nil], + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt: 2], + @"DataElementSize", + [NSNumber numberWithInt: 1], + @"DataElementType", + [NSNumber numberWithInt: 0x0100], // offset + @"DataElementValue", + nil], + nil], + @"0006 - LanguageBaseAttributeIDList", + + // Bluetooth profile descriptor list + [NSArray arrayWithObject: + [NSArray arrayWithObjects: + [IOBluetoothSDPUUID uuid16: kBluetoothSDPUUID16ServiceClassSerialPort], + [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt: 2], + @"DataElementSize", + [NSNumber numberWithInt: 1], + @"DataElementType", + [NSNumber numberWithInt: 0x0100], // version number ? + @"DataElementValue", + nil], + nil]], + @"0009 - BluetoothProfileDescriptorList", + + // Attributes pointed to by the LanguageBaseAttributeIDList + @"LibreOffice Impress Remote Control", + @"0100 - ServiceName", + @"The Document Foundation", + @"0102 - ProviderName", + nil]; + + // Create service + IOBluetoothSDPServiceRecordRef serviceRecordRef; + SAL_WNODEPRECATED_DECLARATIONS_PUSH //TODO: 10.9 IOBluetoothAddServiceDict + IOReturn rc = IOBluetoothAddServiceDict(reinterpret_cast<CFDictionaryRef>(dict), &serviceRecordRef); + SAL_WNODEPRECATED_DECLARATIONS_POP + + SAL_INFO("sdremote.bluetooth", "IOBluetoothAddServiceDict returned " << rc); + + if (rc == kIOReturnSuccess) + { + IOBluetoothSDPServiceRecord *serviceRecord = + [IOBluetoothSDPServiceRecord withSDPServiceRecordRef: serviceRecordRef]; + + BluetoothRFCOMMChannelID channelID; + [serviceRecord getRFCOMMChannelID: &channelID]; + + BluetoothSDPServiceRecordHandle serviceRecordHandle; + [serviceRecord getServiceRecordHandle: &serviceRecordHandle]; + + // Register callback for incoming connections + IOBluetoothRegisterForFilteredRFCOMMChannelOpenNotifications( + incomingCallback, + this, + channelID, + kIOBluetoothUserNotificationChannelDirectionIncoming); + + [serviceRecord release]; + } + + [pool release]; + + (void) mpCommunicators; +#else + (void) mpCommunicators; // avoid warnings about unused member +#endif +} + +BluetoothServer *sd::BluetoothServer::spServer = nullptr; + +void BluetoothServer::setup( std::vector<Communicator*>* pCommunicators ) +{ + if (spServer) + return; + + spServer = new BluetoothServer( pCommunicators ); + spServer->create(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/BluetoothServer.hxx b/sd/source/ui/remotecontrol/BluetoothServer.hxx new file mode 100644 index 000000000..9e20bfa51 --- /dev/null +++ b/sd/source/ui/remotecontrol/BluetoothServer.hxx @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <osl/thread.hxx> +#include <memory> +#include <vector> + +#include <config_dbus.h> + +#if (defined(LINUX) && !defined(__FreeBSD_kernel__)) && ENABLE_DBUS && DBUS_HAVE_GLIB +# define LINUX_BLUETOOTH +#endif + +namespace sd +{ + class Communicator; + + class BluetoothServer: + public osl::Thread + { + public: + static void setup( std::vector<Communicator*>* pCommunicators ); + + /// ensure that Bluetooth discoverability is on + static void ensureDiscoverable(); + /// restore the state of discoverability from before ensureDiscoverable + static void restoreDiscoverable(); + + // called by C / idle callbacks + static void doEnsureDiscoverable(); + static void doRestoreDiscoverable(); + +#if defined(MACOSX) + void addCommunicator( Communicator* pCommunicator ); +#endif + private: + explicit BluetoothServer( std::vector<Communicator*>* pCommunicators ); + virtual ~BluetoothServer() override; + + enum { UNKNOWN, DISCOVERABLE, NOT_DISCOVERABLE } meWasDiscoverable; + static BluetoothServer *spServer; + +#ifdef LINUX_BLUETOOTH + struct Impl; + std::unique_ptr<Impl> mpImpl; +#endif + virtual void SAL_CALL run() override; + + void cleanupCommunicators(); + std::vector<Communicator*>* mpCommunicators; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/BluetoothServer.mm b/sd/source/ui/remotecontrol/BluetoothServer.mm new file mode 100644 index 000000000..28288ff6f --- /dev/null +++ b/sd/source/ui/remotecontrol/BluetoothServer.mm @@ -0,0 +1 @@ +#include "BluetoothServer.cxx"
\ No newline at end of file diff --git a/sd/source/ui/remotecontrol/BluetoothServiceRecord.hxx b/sd/source/ui/remotecontrol/BluetoothServiceRecord.hxx new file mode 100644 index 000000000..c1a00fb3b --- /dev/null +++ b/sd/source/ui/remotecontrol/BluetoothServiceRecord.hxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +// FIXME: look into sharing definitions across OS's (i.e. UUID and port ). +// Look into dynamically determining which ports are available. + +// SDP is a Service Description Protocol cf. +// http://developer.bluetooth.org/TechnologyOverview/Pages/DI.aspx +// This is an XML representation, an alternative would be a +// binary SDP record. + +// for numbers see: +// https://www.bluetooth.org/Technical/AssignedNumbers/service_discovery.htm + +const char * const bluetooth_service_record = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<record>" + "<attribute id=\"0x0001\">" // Service class ID list + "<sequence>" + "<uuid value=\"0x1101\"/>" // an assigned service class meaning: 'serial port' + // we could add our own 'LibreOffice remote' service + // class here too in future ... + "</sequence>" + "</attribute>" + "<attribute id=\"0x0004\">" // Protocol Descriptor list + "<sequence>" + "<sequence>" + "<uuid value=\"0x0100\"/>" // L2CAP Protocol descriptor + "</sequence>" + "<sequence>" + "<uuid value=\"0x0003\"/>" // enumeration value of RFCOMM protocol + "<uint8 value=\"0x05\"/>" // RFCOMM port number + "</sequence>" + "</sequence>" + "</attribute>" + "<attribute id=\"0x0005\">" // Browse Group List + "<sequence>" + "<uuid value=\"0x1002\"/>" // public browse class + "</sequence>" + "</attribute>" + "<attribute id=\"0x0006\">" // Language Base Attribute ID List + "<sequence>" + "<uint16 value=\"0x656e\"/>" // code_ISO639 + "<uint16 value=\"0x006a\"/>" // encoding 0x6a + "<uint16 value=\"0x0100\"/>" // base_offset ie. points to below => + "</sequence>" + "</attribute>" + "<attribute id=\"0x0009\">" // Bluetooth Profile Descriptor List + "<sequence>" + "<sequence>" + "<uuid value=\"0x1101\"/>" // 'serial port' UUID as above + "<uint16 value=\"0x0100\"/>"// version number 1.0 ? + "</sequence>" + "</sequence>" + "</attribute>" + // Attribute identifiers are pointed to by the Language Base Attribute ID List + // id+0 = ServiceName, id+1 = ServiceDescription, id+2=ProviderName + "<attribute id=\"0x0100\">" + "<text value=\"LibreOffice Impress Remote Control\"/>" + "</attribute>" + "<attribute id=\"0x0102\">" + "<text value=\"The Document Foundation\"/>" + "</attribute>" + "</record>" + ; + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/BufferedStreamSocket.cxx b/sd/source/ui/remotecontrol/BufferedStreamSocket.cxx new file mode 100644 index 000000000..64ad5eb8d --- /dev/null +++ b/sd/source/ui/remotecontrol/BufferedStreamSocket.cxx @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "BufferedStreamSocket.hxx" + +#include <osl/socket.hxx> +#include <sal/log.hxx> +#include <algorithm> + +#ifdef _WIN32 + // LO vs WinAPI conflict + #undef WB_LEFT + #undef WB_RIGHT + + #include <winsock2.h> +#else + #include <sys/socket.h> + #include <unistd.h> +#endif +using namespace sd; +using namespace osl; + +BufferedStreamSocket::BufferedStreamSocket( const osl::StreamSocket &aSocket ): + StreamSocket( aSocket ), + aRet( 0 ), + aRead( 0 ), + mSocket( 0 ), + usingCSocket( false ) +{ +} + +BufferedStreamSocket::BufferedStreamSocket( int aSocket ): + aRet( 0 ), + aRead( 0 ), + mSocket( aSocket ), + usingCSocket( true ) +{ +} + +BufferedStreamSocket::~BufferedStreamSocket() { + close(); +} + +void BufferedStreamSocket::getPeerAddr(osl::SocketAddr& rAddr) +{ + assert ( !usingCSocket ); + StreamSocket::getPeerAddr( rAddr ); +} + +sal_Int32 BufferedStreamSocket::write( const void* pBuffer, sal_uInt32 n ) +{ + if ( !usingCSocket ) + return StreamSocket::write( pBuffer, n ); + else + return ::send( + mSocket, +#if defined(_WIN32) + static_cast<char const *>(pBuffer), +#else + pBuffer, +#endif + static_cast<size_t>(n), 0 ); +} + +void BufferedStreamSocket::close() +{ + if( usingCSocket && mSocket != -1 ) + { +#ifdef _WIN32 + ::closesocket( mSocket ); +#else + ::close( mSocket ); +#endif + mSocket = -1; + } + else + ::osl::StreamSocket::close(); +} + +sal_Int32 BufferedStreamSocket::readLine( OString& aLine ) +{ + while ( true ) + { + // Process buffer first in case data already present. + std::vector<char>::iterator aIt; + if ( (aIt = find( aBuffer.begin(), aBuffer.end(), '\n' )) + != aBuffer.end() ) + { + sal_uInt64 aLocation = aIt - aBuffer.begin(); + + aLine = OString( &(*aBuffer.begin()), aLocation ); + + aBuffer.erase( aBuffer.begin(), aIt + 1 ); // Also delete the empty line + aRead -= (aLocation + 1); + + SAL_INFO( "sdremote.bluetooth", "recv line '" << aLine << "'" ); + + return aLine.getLength() + 1; + } + + // Then try and receive if nothing present + aBuffer.resize( aRead + 100 ); + if ( !usingCSocket) + aRet = StreamSocket::recv( &aBuffer[aRead], 100 ); + else + aRet = ::recv( mSocket, &aBuffer[aRead], 100, 0 ); + + SAL_INFO( "sdremote.bluetooth", "recv " << aRet << " aBuffer len " << aBuffer.size() ); + if ( aRet <= 0 ) + { + return 0; + } + // Prevent buffer from growing massively large. + if ( aRead > MAX_LINE_LENGTH ) + { + aBuffer.clear(); + return 0; + } + aRead += aRet; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/BufferedStreamSocket.hxx b/sd/source/ui/remotecontrol/BufferedStreamSocket.hxx new file mode 100644 index 000000000..6abf7ec1b --- /dev/null +++ b/sd/source/ui/remotecontrol/BufferedStreamSocket.hxx @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include "IBluetoothSocket.hxx" +#include <osl/socket_decl.hxx> +#include <vector> + +#define MAX_LINE_LENGTH 20000 + +namespace sd +{ + + /** + * [A wrapper for an osl StreamSocket to allow reading lines.] + * + * Currently wraps either an osl StreamSocket or a standard c socket, + * allowing reading and writing for our purposes. Should eventually be + * returned to being a StreamSocket wrapper if/when Bluetooth is + * integrated into osl Sockets. + */ + class BufferedStreamSocket final : + public IBluetoothSocket, + private ::osl::StreamSocket + { + public: + /** + * Create a BufferedStreamSocket on top of an + * osl::StreamSocket. + */ + explicit BufferedStreamSocket( const osl::StreamSocket &aSocket ); + /** + * Create a BufferedStreamSocket on top of a POSIX or WinSock socket. + */ + explicit BufferedStreamSocket( int aSocket ); + BufferedStreamSocket( const BufferedStreamSocket &aSocket ); + + ~BufferedStreamSocket(); + + /** + * Blocks until a line is read. + * Returns whatever the last call of recv returned, i.e. 0 or less + * if there was a problem in communications. + */ + virtual sal_Int32 readLine( OString& aLine ) override; + + virtual sal_Int32 write( const void* pBuffer, sal_uInt32 n ) override; + + virtual void close() override; + + void getPeerAddr(osl::SocketAddr&); + private: + sal_Int32 aRet, aRead; + std::vector<char> aBuffer; + int mSocket; + bool usingCSocket; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Communicator.cxx b/sd/source/ui/remotecontrol/Communicator.cxx new file mode 100644 index 000000000..59509ed3c --- /dev/null +++ b/sd/source/ui/remotecontrol/Communicator.cxx @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include <algorithm> +#include <vector> + +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/presentation/XPresentation2.hpp> +#include <com/sun/star/presentation/XPresentationSupplier.hpp> +#include <comphelper/processfactory.hxx> +#include <comphelper/documentinfo.hxx> +#include <config_version.h> +#include <rtl/string.hxx> +#include <sal/log.hxx> + +#include "Communicator.hxx" +#include "IBluetoothSocket.hxx" +#include "Listener.hxx" +#include "Receiver.hxx" +#include "Transmitter.hxx" +#include <RemoteServer.hxx> + +using namespace sd; +using namespace com::sun::star; +using namespace osl; + +Communicator::Communicator( std::unique_ptr<IBluetoothSocket> pSocket ): + Thread( "CommunicatorThread" ), + mpSocket( std::move(pSocket) ) +{ +} + +Communicator::~Communicator() +{ +} + +/// Close the underlying socket from another thread to force +/// an early exit / termination +void Communicator::forceClose() +{ + if( mpSocket ) + mpSocket->close(); +} + +// Run as a thread +void Communicator::execute() +{ + pTransmitter.reset( new Transmitter( mpSocket.get() ) ); + pTransmitter->create(); + + pTransmitter->addMessage( "LO_SERVER_SERVER_PAIRED\n\n", + Transmitter::PRIORITY_HIGH ); + + pTransmitter->addMessage( "LO_SERVER_INFO\n" LIBO_VERSION_DOTTED "\n\n", + Transmitter::PRIORITY_HIGH ); + + Receiver aReceiver( pTransmitter.get() ); + try { + uno::Reference< frame::XDesktop2 > xFramesSupplier = frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + uno::Reference< frame::XFrame > xFrame = xFramesSupplier->getActiveFrame(); + + uno::Reference<presentation::XPresentationSupplier> xPS; + if( xFrame.is() ) + xPS.set( xFrame->getController()->getModel(), uno::UNO_QUERY ); + uno::Reference<presentation::XPresentation2> xPresentation; + if( xPS.is() ) + xPresentation.set( xPS->getPresentation(), uno::UNO_QUERY ); + if ( xPresentation.is() && xPresentation->isRunning() ) + { + presentationStarted( xPresentation->getController() ); + OString aBuffer = + "slideshow_info\n" + + OUStringToOString( ::comphelper::DocumentInfo::getDocumentTitle( xFrame->getController()->getModel() ), RTL_TEXTENCODING_UTF8 ) + + "\n\n"; + + pTransmitter->addMessage( aBuffer.getStr(), Transmitter::PRIORITY_LOW ); + } + else + { + pTransmitter->addMessage( "slideshow_finished\n\n", + Transmitter::PRIORITY_HIGH ); + } + } + catch (uno::RuntimeException &) + { + } + + sal_uInt64 aRet; + std::vector<OString> aCommand; + while ( true ) + { + OString aLine; + aRet = mpSocket->readLine( aLine ); + if ( aRet == 0 ) + { + break; // I.e. transmission finished. + } + if ( aLine.getLength() ) + { + aCommand.push_back( aLine ); + } + else + { + aReceiver.pushCommand( aCommand ); + aCommand.clear(); + } + } + + SAL_INFO ("sdremote", "Exiting transmission loop"); + + disposeListener(); + + pTransmitter->notifyFinished(); + pTransmitter->join(); + pTransmitter = nullptr; + + mpSocket->close(); + mpSocket.reset(); + + RemoteServer::removeCommunicator( this ); +} + +void Communicator::informListenerDestroyed() +{ + if ( pTransmitter ) + pTransmitter->addMessage( "slideshow_finished\n\n", + Transmitter::PRIORITY_HIGH ); +} + +void Communicator::presentationStarted( const css::uno::Reference< + css::presentation::XSlideShowController > &rController ) +{ + if ( pTransmitter ) + { + mListener.set( new Listener( this, pTransmitter.get() ) ); + mListener->init( rController ); + } +} + +void Communicator::disposeListener() +{ + if ( mListener.is() ) + { + mListener->dispose(); + mListener = nullptr; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Communicator.hxx b/sd/source/ui/remotecontrol/Communicator.hxx new file mode 100644 index 000000000..f8f23c58c --- /dev/null +++ b/sd/source/ui/remotecontrol/Communicator.hxx @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <memory> + +#include <rtl/ref.hxx> +#include <salhelper/thread.hxx> + +namespace com::sun::star::uno { template <typename > class Reference; } +namespace com::sun::star::presentation { class XSlideShowController; } +namespace sd { struct IBluetoothSocket; } + +namespace sd +{ + + class Transmitter; + class Listener; + + /** Class used for communication with one single client, dealing with all + * tasks specific to this client. + * + * Needs to be created, then started using launch(), disposes itself. + */ + class Communicator : public salhelper::Thread + { + public: + explicit Communicator( std::unique_ptr<IBluetoothSocket> pSocket ); + virtual ~Communicator() override; + + void presentationStarted( const css::uno::Reference< + css::presentation::XSlideShowController > &rController ); + void informListenerDestroyed(); + void disposeListener(); + void forceClose(); + + private: + void execute() override; + std::unique_ptr<IBluetoothSocket> mpSocket; + + std::unique_ptr<Transmitter> pTransmitter; + rtl::Reference<Listener> mListener; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/DiscoveryService.cxx b/sd/source/ui/remotecontrol/DiscoveryService.cxx new file mode 100644 index 000000000..bdd0b51c8 --- /dev/null +++ b/sd/source/ui/remotecontrol/DiscoveryService.cxx @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <errno.h> +#include <string.h> +#include <iostream> + +#include <osl/socket.hxx> +#include <config_features.h> +#include <sal/log.hxx> + +#include "DiscoveryService.hxx" +#include "ZeroconfService.hxx" + +#ifdef _WIN32 + // LO vs WinAPI conflict + #undef WB_LEFT + #undef WB_RIGHT + + #include <winsock2.h> + #include <ws2tcpip.h> + + #include "WINNetworkService.hxx" + typedef int socklen_t; +#else + #include <unistd.h> + #include <sys/socket.h> + #include <netinet/in.h> +#endif + +#ifdef MACOSX + #include <osl/conditn.hxx> + #include <premac.h> + #import <CoreFoundation/CoreFoundation.h> + #include <postmac.h> + #import "OSXNetworkService.hxx" +#endif + +#if HAVE_FEATURE_AVAHI + #include "AvahiNetworkService.hxx" +#endif + +using namespace osl; +using namespace sd; + +DiscoveryService::DiscoveryService() + : mSocket(-1) + , zService(nullptr) +{ +} + +DiscoveryService::~DiscoveryService() +{ + if (mSocket != -1) + { +#ifdef _WIN32 + closesocket( mSocket ); +#else + close( mSocket ); +#endif + } + + if (zService) + zService->clear(); +} + +void DiscoveryService::setupSockets() +{ + +#ifdef MACOSX + // Bonjour for OSX + zService = new OSXNetworkService(); + zService->setup(); +#endif + +#if HAVE_FEATURE_AVAHI + // Avahi for Linux + char hostname[1024]; + hostname[1023] = '\0'; + gethostname(hostname, 1023); + + zService = new AvahiNetworkService(hostname); + zService->setup(); +#endif + +#ifdef _WIN32 + zService = new WINNetworkService(); + zService->setup(); +#endif + + // Old implementation for backward compatibility matter + mSocket = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); + if (mSocket == -1) + { + SAL_WARN("sd", "DiscoveryService: socket failed: " << errno); + return; // would be better to throw, but unsure if caller handles that + } + + sockaddr_in aAddr = {}; + aAddr.sin_family = AF_INET; + aAddr.sin_addr.s_addr = htonl(INADDR_ANY); + aAddr.sin_port = htons( PORT_DISCOVERY ); + + int rc = bind( mSocket, reinterpret_cast<sockaddr*>(&aAddr), sizeof(sockaddr_in) ); + + if (rc) + { + SAL_WARN("sd", "DiscoveryService: bind failed: " << errno); + return; // would be better to throw, but unsure if caller handles that + } + + struct ip_mreq multicastRequest; + + multicastRequest.imr_multiaddr.s_addr = htonl((239U << 24) | 1U); // 239.0.0.1 + multicastRequest.imr_interface.s_addr = htonl(INADDR_ANY); + + rc = setsockopt( mSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, + #ifdef _WIN32 + reinterpret_cast<const char*>(&multicastRequest), + #else + &multicastRequest, + #endif + sizeof(multicastRequest)); + + if (rc) + { + SAL_WARN("sd", "DiscoveryService: setsockopt failed: " << errno); + return; // would be better to throw, but unsure if caller handles that + } +} + +void SAL_CALL DiscoveryService::run() +{ + osl::Thread::setName("DiscoveryService"); + + setupSockets(); + + // Kept for backward compatibility + while ( true ) + { + char aBuffer[BUFFER_SIZE] = {}; + sockaddr_in aAddr; + socklen_t aLen = sizeof( aAddr ); + if(recvfrom( mSocket, aBuffer, BUFFER_SIZE, 0, reinterpret_cast<sockaddr*>(&aAddr), &aLen ) > 0) + { + OString aString( aBuffer, strlen( "LOREMOTE_SEARCH" ) ); + if ( aString == "LOREMOTE_SEARCH" ) + { + OString aStringBuffer = "LOREMOTE_ADVERTISE\n" + + OUStringToOString(osl::SocketAddr::getLocalHostname(), RTL_TEXTENCODING_UTF8 ) + + "\n\n"; + if ( sendto( mSocket, aStringBuffer.getStr(), + aStringBuffer.getLength(), 0, reinterpret_cast<sockaddr*>(&aAddr), + sizeof(aAddr) ) <= 0 ) + { + // Write error or closed socket -- we are done. + return; + } + } + } + else + { + // Read error or closed socket -- we are done. + return; + } + } +} + +DiscoveryService *sd::DiscoveryService::spService = nullptr; + +void DiscoveryService::setup() +{ + if (spService) + return; + + spService = new DiscoveryService(); + spService->create(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/DiscoveryService.hxx b/sd/source/ui/remotecontrol/DiscoveryService.hxx new file mode 100644 index 000000000..4b235fe89 --- /dev/null +++ b/sd/source/ui/remotecontrol/DiscoveryService.hxx @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <osl/thread.hxx> + +namespace sd { class ZeroconfService; } + +namespace sd +{ + class DiscoveryService : public osl::Thread + { + public: + static void setup(); + + private: + DiscoveryService(); + virtual ~DiscoveryService() override; + + /** + * Networking related setup -- must be run within our own thread + * to prevent the application blocking (fdo#75328). + */ + void setupSockets(); + + static DiscoveryService *spService; + virtual void SAL_CALL run() override; + int mSocket; + + ZeroconfService * zService; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/DiscoveryService.mm b/sd/source/ui/remotecontrol/DiscoveryService.mm new file mode 100644 index 000000000..3cad7cdfb --- /dev/null +++ b/sd/source/ui/remotecontrol/DiscoveryService.mm @@ -0,0 +1 @@ +#include "DiscoveryService.cxx"
\ No newline at end of file diff --git a/sd/source/ui/remotecontrol/IBluetoothSocket.hxx b/sd/source/ui/remotecontrol/IBluetoothSocket.hxx new file mode 100644 index 000000000..4b75a1e82 --- /dev/null +++ b/sd/source/ui/remotecontrol/IBluetoothSocket.hxx @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <rtl/string.hxx> + +namespace sd +{ + /** Interface for bluetooth data io + */ + struct IBluetoothSocket + { + IBluetoothSocket() = default; + virtual ~IBluetoothSocket() {} + IBluetoothSocket(const IBluetoothSocket&) = delete; + IBluetoothSocket& operator=(const IBluetoothSocket&) = delete; + + /** Blocks until a line is read. + + @return whatever the last call of recv returned, i.e. 0 or less + if there was a problem in communications. + */ + virtual sal_Int32 readLine(OString& aLine) = 0; + + /** Write a number of bytes + + @return number of bytes actually written + */ + virtual sal_Int32 write( const void* pBuffer, sal_uInt32 n ) = 0; + + virtual void close() {}; + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/ImagePreparer.cxx b/sd/source/ui/remotecontrol/ImagePreparer.cxx new file mode 100644 index 000000000..ba8d2c1f3 --- /dev/null +++ b/sd/source/ui/remotecontrol/ImagePreparer.cxx @@ -0,0 +1,255 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include "ImagePreparer.hxx" +#include "Transmitter.hxx" + +#include <comphelper/base64.hxx> +#include <comphelper/processfactory.hxx> +#include <comphelper/propertyvalue.hxx> +#include <osl/file.hxx> +#include <rtl/ustrbuf.hxx> +#include <sal/log.hxx> + +#include <com/sun/star/beans/PropertyValue.hpp> +#include <com/sun/star/drawing/GraphicExportFilter.hpp> +#include <com/sun/star/lang/XServiceName.hpp> +#include <com/sun/star/presentation/XSlideShowController.hpp> +#include <com/sun/star/presentation/XPresentationPage.hpp> +#include <com/sun/star/text/XTextRange.hpp> + +using namespace ::sd; +using namespace ::osl; +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; + +ImagePreparer::ImagePreparer( + const uno::Reference<presentation::XSlideShowController>& rxController, + Transmitter *aTransmitter ) + : Timer("sd ImagePreparer"), + xController( rxController ), + pTransmitter( aTransmitter ) +{ + SAL_INFO( "sdremote", "ImagePreparer - start" ); + SetTimeout( 50 ); + mnSendingSlide = 0; + Start(); +} + +ImagePreparer::~ImagePreparer() +{ + SAL_INFO( "sdremote", "ImagePreparer - stop" ); + Stop(); +} + +void ImagePreparer::Invoke() +{ + sal_uInt32 aSlides = xController->getSlideCount(); + SAL_INFO( "sdremote", "ImagePreparer " << xController->isRunning() << + " sending slide " << mnSendingSlide << " of " << aSlides ); + if ( xController->isRunning() && // not stopped/disposed of. + mnSendingSlide < aSlides ) + { + sendPreview( mnSendingSlide ); + sendNotes( mnSendingSlide ); + mnSendingSlide++; + Start(); + } + else + Stop(); +} + +void ImagePreparer::sendPreview( sal_uInt32 aSlideNumber ) +{ + sal_uInt64 aSize; + uno::Sequence<sal_Int8> aImageData = preparePreview( aSlideNumber, 320, 240, + aSize ); + if ( !xController->isRunning() ) + return; + + OUStringBuffer aStrBuffer; + ::comphelper::Base64::encode( aStrBuffer, aImageData ); + + OString aEncodedShortString = OUStringToOString( + aStrBuffer.makeStringAndClear(), RTL_TEXTENCODING_UTF8 ); + + // Start the writing + OString aBuffer = "slide_preview\n" + + OString::number(aSlideNumber) + + "\n" + aEncodedShortString + "\n\n"; + pTransmitter->addMessage( aBuffer, + Transmitter::PRIORITY_LOW ); + +} + +uno::Sequence<sal_Int8> ImagePreparer::preparePreview( + sal_uInt32 aSlideNumber, sal_uInt32 aWidth, sal_uInt32 aHeight, + sal_uInt64 &rSize ) +{ + OUString aFileURL; + FileBase::createTempFile( nullptr, nullptr, &aFileURL ); + + uno::Reference< drawing::XGraphicExportFilter > xFilter = + drawing::GraphicExportFilter::create( ::comphelper::getProcessComponentContext() ); + + if ( !xController->isRunning() ) + return uno::Sequence<sal_Int8>(); + + uno::Reference< lang::XComponent > xSourceDoc( + xController->getSlideByIndex( aSlideNumber ), + uno::UNO_QUERY_THROW ); + + xFilter->setSourceDocument( xSourceDoc ); + + uno::Sequence< beans::PropertyValue > aFilterData{ + comphelper::makePropertyValue("PixelWidth", aWidth), + comphelper::makePropertyValue("PixelHeight", aHeight), + comphelper::makePropertyValue("ColorMode", sal_Int32(0)) // 0: Color, 1: B&W + }; + + uno::Sequence< beans::PropertyValue > aProps{ + comphelper::makePropertyValue("MediaType", OUString( "image/png" )), + comphelper::makePropertyValue("URL", aFileURL), + comphelper::makePropertyValue("FilterData", aFilterData) + }; + + xFilter->filter( aProps ); + + File aFile(aFileURL); + if (aFile.open(0) != osl::File::E_None) + return uno::Sequence<sal_Int8>(); + + sal_uInt64 aRead; + rSize = 0; + aFile.getSize( rSize ); + uno::Sequence<sal_Int8> aContents( rSize ); + + aFile.read( aContents.getArray(), rSize, aRead ); + if (aRead != rSize) + aContents.realloc(aRead); + + aFile.close(); + File::remove( aFileURL ); + return aContents; + +} + +void ImagePreparer::sendNotes( sal_uInt32 aSlideNumber ) +{ + + OString aNotes = prepareNotes( aSlideNumber ); + + if ( aNotes.isEmpty() ) + return; + + if ( !xController->isRunning() ) + return; + + // Start the writing + OString aBuffer = + "slide_notes\n" + + OString::number( static_cast<sal_Int32>(aSlideNumber) ) + + "\n" + "<html><body>" + + aNotes + + "</body></html>" + "\n\n"; + pTransmitter->addMessage( aBuffer, + Transmitter::PRIORITY_LOW ); +} + +// Code copied from sdremote/source/presenter/PresenterNotesView.cxx +OString ImagePreparer::prepareNotes( sal_uInt32 aSlideNumber ) +{ + OUStringBuffer aRet; + + if ( !xController->isRunning() ) + return ""; + + uno::Reference<css::drawing::XDrawPage> aNotesPage; + uno::Reference< drawing::XDrawPage > xSourceDoc( + xController->getSlideByIndex( aSlideNumber ), + uno::UNO_SET_THROW ); + uno::Reference<presentation::XPresentationPage> xPresentationPage( + xSourceDoc, UNO_QUERY); + if (xPresentationPage.is()) + aNotesPage = xPresentationPage->getNotesPage(); + else + return ""; + + static constexpr OUStringLiteral sNotesShapeName ( + u"com.sun.star.presentation.NotesShape" ); + static constexpr OUStringLiteral sTextShapeName ( + u"com.sun.star.drawing.TextShape" ); + + if (aNotesPage.is()) + { + + // Iterate over all shapes and find the one that holds the text. + sal_Int32 nCount (aNotesPage->getCount()); + for (sal_Int32 nIndex=0; nIndex<nCount; ++nIndex) + { + + uno::Reference<lang::XServiceName> xServiceName ( + aNotesPage->getByIndex(nIndex), UNO_QUERY); + if (xServiceName.is() + && xServiceName->getServiceName() == sNotesShapeName) + { + uno::Reference<text::XTextRange> xText (xServiceName, UNO_QUERY); + if (xText.is()) + { + aRet.append(xText->getString()); + aRet.append("<br/>"); + } + } + else + { + uno::Reference<drawing::XShapeDescriptor> xShapeDescriptor ( + aNotesPage->getByIndex(nIndex), UNO_QUERY); + if (xShapeDescriptor.is()) + { + OUString sType (xShapeDescriptor->getShapeType()); + if (sType == sNotesShapeName || sType == sTextShapeName) + { + uno::Reference<text::XTextRange> xText ( + aNotesPage->getByIndex(nIndex), UNO_QUERY); + if (xText.is()) + { + aRet.append(xText->getString()); + aRet.append("<br/>"); + } + } + } + } + } + } + // Replace all newlines with <br\> tags + for ( sal_Int32 i = 0; i < aRet.getLength(); i++ ) + { + if ( aRet[i] == '\n' ) + { + aRet[i]= '<'; + aRet.insert( i+1, "br/>" ); + } + } + return OUStringToOString( + aRet.makeStringAndClear(), RTL_TEXTENCODING_UTF8 ); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/ImagePreparer.hxx b/sd/source/ui/remotecontrol/ImagePreparer.hxx new file mode 100644 index 000000000..146eba073 --- /dev/null +++ b/sd/source/ui/remotecontrol/ImagePreparer.hxx @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <vcl/timer.hxx> +#include <com/sun/star/uno/Reference.hxx> + +namespace com::sun::star::presentation { class XSlideShowController; } +namespace sd { class Transmitter; } + +namespace sd +{ + +class ImagePreparer : private Timer +{ + sal_uInt32 mnSendingSlide; +public: + ImagePreparer( const + css::uno::Reference<css::presentation::XSlideShowController>& + rxController, sd::Transmitter *aTransmitter ); + virtual ~ImagePreparer() override; + +private: + css::uno::Reference<css::presentation::XSlideShowController> xController; + Transmitter *pTransmitter; + + virtual void Invoke() override; + + void sendPreview( sal_uInt32 aSlideNumber ); + css::uno::Sequence<sal_Int8> preparePreview( sal_uInt32 aSlideNumber, + sal_uInt32 aWidth, sal_uInt32 aHeight, sal_uInt64 &rSize ); + + void sendNotes( sal_uInt32 aSlideNumber ); + OString prepareNotes( sal_uInt32 aSlideNumber ); +}; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Listener.cxx b/sd/source/ui/remotecontrol/Listener.cxx new file mode 100644 index 000000000..3753ed9b5 --- /dev/null +++ b/sd/source/ui/remotecontrol/Listener.cxx @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/log.hxx> +#include <vcl/svapp.hxx> + +#include "Communicator.hxx" +#include "Listener.hxx" +#include "ImagePreparer.hxx" +#include "Transmitter.hxx" + +#include <com/sun/star/presentation/XSlideShowController.hpp> + +using namespace sd; +using namespace ::com::sun::star::presentation; + +Listener::Listener( const ::rtl::Reference<Communicator>& rCommunicator, + sd::Transmitter *aTransmitter ): + mCommunicator( rCommunicator ), + pTransmitter( nullptr ) +{ + pTransmitter = aTransmitter; +} + +Listener::~Listener() +{ +} + +void Listener::init( const css::uno::Reference< css::presentation::XSlideShowController >& aController) +{ + if ( aController.is() ) + { + mController.set( aController ); + aController->addSlideShowListener( this ); + + sal_Int32 aSlides = aController->getSlideCount(); + sal_Int32 aCurrentSlide = aController->getCurrentSlideIndex(); + OString aBuffer = "slideshow_started\n" + + OString::number( aSlides ) + "\n" + + OString::number( aCurrentSlide ) + "\n\n"; + + pTransmitter->addMessage( aBuffer, + Transmitter::PRIORITY_HIGH ); + + { + SolarMutexGuard aGuard; + /* ImagePreparer* pPreparer = */ new ImagePreparer( aController, pTransmitter ); + } + } + else + { + SAL_INFO( "sdremote", "Listener::init but no controller - so no preview push queued" ); + } +} + +//----- XAnimationListener ---------------------------------------------------- + +void SAL_CALL Listener::beginEvent(const css::uno::Reference< + css::animations::XAnimationNode >& ) +{} + +void SAL_CALL Listener::endEvent( const css::uno::Reference< + css::animations::XAnimationNode >& ) +{} + +void SAL_CALL Listener::repeat( const css::uno::Reference< + css::animations::XAnimationNode >&, ::sal_Int32 ) +{} + +//----- XSlideShowListener ---------------------------------------------------- + +void SAL_CALL Listener::paused() +{ +} + +void SAL_CALL Listener::resumed() +{ +} + +void SAL_CALL Listener::slideEnded (sal_Bool) +{ +} + +void SAL_CALL Listener::hyperLinkClicked (const OUString &) +{ +} + +void SAL_CALL Listener::slideTransitionStarted() +{ + sal_Int32 aSlide = mController->getCurrentSlideIndex(); + + OString aBuilder = "slide_updated\n" + + OString::number( aSlide ) + + "\n\n"; + + if ( pTransmitter ) + { + pTransmitter->addMessage( aBuilder, + Transmitter::PRIORITY_HIGH ); + } +} + +void SAL_CALL Listener::slideTransitionEnded() +{ +} + +void SAL_CALL Listener::slideAnimationsEnded() +{ +} + +void Listener::disposing(std::unique_lock<std::mutex>&) +{ + pTransmitter = nullptr; + if ( mController.is() ) + { + mController->removeSlideShowListener( this ); + mController = nullptr; + } + mCommunicator->informListenerDestroyed(); +} + +void SAL_CALL Listener::disposing ( + const css::lang::EventObject&) +{ + dispose(); +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Listener.hxx b/sd/source/ui/remotecontrol/Listener.hxx new file mode 100644 index 000000000..58d7483f6 --- /dev/null +++ b/sd/source/ui/remotecontrol/Listener.hxx @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <sal/config.h> +#include <com/sun/star/presentation/XSlideShowListener.hpp> + +#include <rtl/ref.hxx> +#include <comphelper/compbase.hxx> +#include <com/sun/star/uno/Reference.hxx> + +namespace com::sun::star::presentation { class XSlideShowController; } +namespace sd { class Communicator; } +namespace sd { class Transmitter; } + +namespace sd { +/** + * Slide show listener. This class can also be used for anything else that is + * specific to the lifetime of one slideshow while a client is connected. + */ +class Listener + : public comphelper::WeakComponentImplHelper< css::presentation::XSlideShowListener > +{ +public: + Listener( const ::rtl::Reference<Communicator>& rServer, sd::Transmitter *aTransmitter ); + virtual ~Listener() override; + void init( const css::uno::Reference< css::presentation::XSlideShowController >& aController ); + + // XAnimationListener + virtual void SAL_CALL beginEvent(const css::uno::Reference< + css::animations::XAnimationNode >& rNode ) override; + virtual void SAL_CALL endEvent( const css::uno::Reference< + css::animations::XAnimationNode >& rNode ) override; + virtual void SAL_CALL repeat( const css::uno::Reference< + css::animations::XAnimationNode >& rNode, ::sal_Int32 Repeat ) override; + + // XSlideShowListener + virtual void SAL_CALL paused( ) override; + virtual void SAL_CALL resumed( ) override; + virtual void SAL_CALL slideTransitionStarted( ) override; + virtual void SAL_CALL slideTransitionEnded( ) override; + virtual void SAL_CALL slideAnimationsEnded( ) override; + virtual void SAL_CALL slideEnded(sal_Bool bReverse) override; + virtual void SAL_CALL hyperLinkClicked( const OUString& hyperLink ) override; + + // XEventListener + virtual void disposing(std::unique_lock<std::mutex>&) override; + virtual void SAL_CALL disposing (const css::lang::EventObject& rEvent) override; + +private: + rtl::Reference<Communicator> mCommunicator; + sd::Transmitter *pTransmitter; + css::uno::Reference< css::presentation::XSlideShowController > mController; +}; +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/OSXBluetooth.h b/sd/source/ui/remotecontrol/OSXBluetooth.h new file mode 100644 index 000000000..64f095f6c --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXBluetooth.h @@ -0,0 +1,30 @@ +/* -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#import <IOBluetooth/objc/IOBluetoothRFCOMMChannel.h> + +#include "IBluetoothSocket.hxx" +#include "Communicator.hxx" +#include "OSXBluetoothWrapper.hxx" + +@interface ChannelDelegate : NSObject<IOBluetoothRFCOMMChannelDelegate> +{ + sd::Communicator* pCommunicator; + sd::OSXBluetoothWrapper* pSocket; +} + +- (id) initWithCommunicatorAndSocket:(sd::Communicator*)communicator socket:(sd::OSXBluetoothWrapper*)socket; +- (void) rfcommChannelData:(IOBluetoothRFCOMMChannel*)rfcommChannel data:(void *)dataPointer length:(size_t)dataLength; +- (void) rfcommChannelClosed:(IOBluetoothRFCOMMChannel*)rfcommChannel; + +@end + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/OSXBluetooth.mm b/sd/source/ui/remotecontrol/OSXBluetooth.mm new file mode 100644 index 000000000..8b705c50b --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXBluetooth.mm @@ -0,0 +1,53 @@ +/* -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include <osl/conditn.hxx> +#include <sal/log.hxx> + +#include <premac.h> +#import <IOBluetooth/objc/IOBluetoothRFCOMMChannel.h> +#include <postmac.h> + +#include "OSXBluetooth.h" + +@implementation ChannelDelegate + +- (id) initWithCommunicatorAndSocket:(sd::Communicator*)communicator socket:(sd::OSXBluetoothWrapper*)socket +{ + pCommunicator = communicator; + pSocket = socket; + return self; +} + +- (void) rfcommChannelData:(IOBluetoothRFCOMMChannel*)rfcommChannel data:(void *)dataPointer length:(size_t)dataLength +{ + (void) rfcommChannel; + + if ( pSocket ) + { + pSocket->appendData(dataPointer, dataLength); + } +} + +- (void) rfcommChannelClosed:(IOBluetoothRFCOMMChannel*)rfcommChannel +{ + (void) rfcommChannel; + + SAL_INFO( "sdremote.bluetooth", "ChannelDelegate::rfcommChannelClosed()"); + + if ( pSocket ) + { + pSocket->channelClosed(); + } + pCommunicator = nullptr; + pSocket = nullptr; +} + +@end + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/OSXBluetoothWrapper.hxx b/sd/source/ui/remotecontrol/OSXBluetoothWrapper.hxx new file mode 100644 index 000000000..26e1349f0 --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXBluetoothWrapper.hxx @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <rtl/string.hxx> +#include <osl/conditn.hxx> +#include <osl/mutex.hxx> +#include <vector> + +#include "IBluetoothSocket.hxx" + +namespace sd +{ + class OSXBluetoothWrapper : public IBluetoothSocket + { + IOBluetoothRFCOMMChannel* mpChannel; + int mnMTU; + osl::Condition mHaveBytes; + osl::Mutex mMutex; + std::vector<char> mBuffer; + + public: + OSXBluetoothWrapper( IOBluetoothRFCOMMChannel* channel ); + virtual sal_Int32 readLine( OString& aLine ) override; + virtual sal_Int32 write( const void* pBuffer, sal_uInt32 len ) override; + void appendData(void* pBuffer, size_t len ); + void channelClosed(); + }; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/OSXNetworkService.h b/sd/source/ui/remotecontrol/OSXNetworkService.h new file mode 100644 index 000000000..7298d901b --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXNetworkService.h @@ -0,0 +1,30 @@ +/* -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <sys/socket.h> +#include <netinet/in.h> + +#include <premac.h> +#import <CoreFoundation/CoreFoundation.h> +#import <Foundation/NSNetServices.h> +#import <Foundation/NSRunLoop.h> +#include <postmac.h> + +@interface OSXBonjourService : NSObject <NSNetServiceDelegate> +{ + NSNetService* netService; +} + +- (void)publishImpressRemoteServiceOnLocalNetworkWithName:(NSString*)sName; + +@end + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/OSXNetworkService.hxx b/sd/source/ui/remotecontrol/OSXNetworkService.hxx new file mode 100644 index 000000000..78ab13eff --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXNetworkService.hxx @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <algorithm> +#include <vector> +#include <iostream> + +#include <osl/conditn.hxx> +#include <premac.h> +#import <CoreFoundation/CoreFoundation.h> +#include <postmac.h> +#import "OSXNetworkService.h" + +#include "ZeroconfService.hxx" + +namespace sd { + class OSXNetworkService : public ZeroconfService + { + private: + OSXBonjourService *osxservice; + public: + OSXNetworkService(const std::string& aname = "", unsigned int aport = 1599) + : ZeroconfService(aname, aport){} + + void clear() override { + [osxservice dealloc]; + } + void setup() override { + osxservice = [[OSXBonjourService alloc] init]; + [osxservice publishImpressRemoteServiceOnLocalNetworkWithName: @""]; + }; + }; +} diff --git a/sd/source/ui/remotecontrol/OSXNetworkService.mm b/sd/source/ui/remotecontrol/OSXNetworkService.mm new file mode 100644 index 000000000..51cbd8c99 --- /dev/null +++ b/sd/source/ui/remotecontrol/OSXNetworkService.mm @@ -0,0 +1,43 @@ +/* -*- Mode: ObjC; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include <osl/conditn.hxx> + +#include <premac.h> + #import <CoreFoundation/CoreFoundation.h> + #import "OSXNetworkService.h" +#include <postmac.h> + +@implementation OSXBonjourService + +- (void) publishImpressRemoteServiceOnLocalNetworkWithName:(NSString *)sName +{ + netService = [[NSNetService alloc] initWithDomain:@"local" type:@"_impressremote._tcp" name:sName port:1599]; + + if (netService != nil) + { + [netService setDelegate:self]; + [netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [netService publish]; + } +} + +-(void)netService:(NSNetService *)aNetService + didNotPublish:(NSDictionary *)dict { + NSLog(@"Service %p did not publish: %@", aNetService, dict); +} + +- (void)dealloc { + [netService stop]; + [netService release]; + [super dealloc]; +} + +@end + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Receiver.cxx b/sd/source/ui/remotecontrol/Receiver.cxx new file mode 100644 index 000000000..dd92e8e99 --- /dev/null +++ b/sd/source/ui/remotecontrol/Receiver.cxx @@ -0,0 +1,207 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include "Receiver.hxx" +#include <com/sun/star/presentation/XSlideShowController.hpp> +#include <com/sun/star/presentation/XPresentationSupplier.hpp> +#include <com/sun/star/presentation/XPresentation2.hpp> +#include <com/sun/star/frame/Desktop.hpp> +#include <com/sun/star/uno/RuntimeException.hpp> + +#include <comphelper/processfactory.hxx> +#include <sal/log.hxx> +#include <com/sun/star/beans/PropertyValue.hpp> +#include <vcl/svapp.hxx> +#include <tools/diagnose_ex.h> + +using namespace sd; +using namespace ::osl; +using namespace ::com::sun::star; +using namespace ::com::sun::star::lang; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::presentation; +using namespace ::com::sun::star::beans; + +Receiver::Receiver( Transmitter *aTransmitter ) : Timer("sd Receiver") +{ + pTransmitter = aTransmitter; + SetTimeout( 0 ); +} + +Receiver::~Receiver() +{ +} + +// Bounce the commands to the main thread to avoid threading woes +void Receiver::pushCommand( const std::vector<OString> &rCommand ) +{ + SolarMutexGuard aGuard; + maExecQueue.push_back( rCommand ); + Start(); +} + +void Receiver::Invoke() +{ + if( !maExecQueue.empty() ) + { + std::vector< OString > aCommands( maExecQueue.front() ); + maExecQueue.pop_front(); + if( !aCommands.empty() ) + executeCommand( aCommands ); + Start(); + } + else + Stop(); +} + +void Receiver::executeCommand( const std::vector<OString> &aCommand ) +{ + uno::Reference<presentation::XSlideShowController> xSlideShowController; + uno::Reference<presentation::XPresentation2> xPresentation; + uno::Reference<presentation::XSlideShow> xSlideShow; + try { + uno::Reference< frame::XDesktop2 > xFramesSupplier = frame::Desktop::create( ::comphelper::getProcessComponentContext() ); + uno::Reference< frame::XFrame > xFrame ( xFramesSupplier->getActiveFrame(), uno::UNO_SET_THROW ); + uno::Reference<presentation::XPresentationSupplier> xPS ( xFrame->getController()->getModel(), uno::UNO_QUERY_THROW); + xPresentation.set( xPS->getPresentation(), uno::UNO_QUERY_THROW); + // Throws an exception if no slideshow running + xSlideShowController.set( xPresentation->getController(), uno::UNO_SET_THROW ); + xSlideShow.set( xSlideShowController->getSlideShow(), uno::UNO_SET_THROW ); + } + catch (uno::RuntimeException &) + { + } + + if ( aCommand[0] == "transition_next" ) + { + if ( xSlideShowController.is() ) + xSlideShowController->gotoNextEffect(); + } + else if ( aCommand[0] == "transition_previous" ) + { + if ( xSlideShowController.is() ) + xSlideShowController->gotoPreviousEffect(); + } + else if ( aCommand[0] == "goto_slide" ) + { + // FIXME: if 0 returned, then not a valid number + sal_Int32 aSlide = aCommand[1].toInt32(); + if ( xSlideShowController.is() && + xSlideShowController->getCurrentSlideIndex() != aSlide ) + { + xSlideShowController->gotoSlideIndex( aSlide ); + } + } + else if ( aCommand[0] == "presentation_start" ) + { + if ( xPresentation.is() ) + xPresentation->start(); + } + else if ( aCommand[0] == "presentation_stop" ) + { + if ( xPresentation.is() ) + xPresentation->end(); + } + else if ( aCommand[0] == "presentation_blank_screen" ) + { + if ( aCommand.size() > 1 ) + { +// aColour = FIXME: get the colour in some format from this string +// Determine the formatting first. + } + if ( xSlideShowController.is() ) + { + xSlideShowController->blankScreen( 0 ); // Default is black + } + } + else if (aCommand[0] == "pointer_started" ) + { + // std::cerr << "pointer_started" << std::endl; + float x = aCommand[1].toFloat(); + float y = aCommand[2].toFloat(); + SolarMutexGuard aSolarGuard; + + const css::geometry::RealPoint2D pos(x,y); + // std::cerr << "Pointer at ("<<pos.X<<","<<pos.Y<<")" << std::endl; + + if (xSlideShow.is()) + { + try + { + // std::cerr << "pointer_coordination in the is" << std::endl; + xSlideShow->setProperty(beans::PropertyValue("PointerPosition", -1, Any(pos), + beans::PropertyState_DIRECT_VALUE)); + } + catch (Exception&) + { + TOOLS_WARN_EXCEPTION("sdremote", "sd::SlideShowImpl::setPointerPosition()"); + } + + try + { + xSlideShow->setProperty(beans::PropertyValue("PointerVisible", -1, Any(true), + beans::PropertyState_DIRECT_VALUE)); + } + catch (Exception&) + { + TOOLS_WARN_EXCEPTION("sdremote", "sd::SlideShowImpl::setPointerMode()"); + } + } + + SAL_INFO( "sdremote", "Pointer started, we display the pointer on screen" ); + } + else if (aCommand[0] == "pointer_dismissed" ) + { + SolarMutexGuard aSolarGuard; + if (xSlideShow.is()) try + { + xSlideShow->setProperty( + beans::PropertyValue( "PointerVisible" , + -1, + Any( false ), + beans::PropertyState_DIRECT_VALUE ) ); + } + catch ( Exception& ) + { + TOOLS_WARN_EXCEPTION( "sdremote", "sd::SlideShowImpl::setPointerMode()" ); + } + + SAL_INFO( "sdremote", "Pointer dismissed, we hide the pointer on screen" ); + } + else if (aCommand[0] == "pointer_coordination" ) + { + float x = aCommand[1].toFloat(); + float y = aCommand[2].toFloat(); + + SAL_INFO( "sdremote", "Pointer at ("<<x<<","<<y<<")" ); + const css::geometry::RealPoint2D pos(x,y); + + SolarMutexGuard aSolarGuard; + if (xSlideShow.is()) try + { + xSlideShow->setProperty( + beans::PropertyValue( "PointerPosition" , + -1, + Any( pos ), + beans::PropertyState_DIRECT_VALUE ) ); + } + catch ( Exception& ) + { + TOOLS_WARN_EXCEPTION( "sdremote", "sd::SlideShowImpl::setPointerPosition()" ); + } + } + else if ( aCommand[0] == "presentation_resume" ) + { + if ( xSlideShowController.is() ) + { + xSlideShowController->resume(); + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Receiver.hxx b/sd/source/ui/remotecontrol/Receiver.hxx new file mode 100644 index 000000000..d3fadf0da --- /dev/null +++ b/sd/source/ui/remotecontrol/Receiver.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#pragma once + +#include <rtl/string.hxx> +#include <vcl/timer.hxx> + +#include <vector> +#include <deque> + +namespace sd { class Transmitter; } + +namespace sd +{ + +// Timer is protected by the solar mutex => so are we. +class Receiver : private Timer +{ + std::deque< std::vector< OString > > maExecQueue; +public: + explicit Receiver( Transmitter *aTransmitter ); + virtual ~Receiver() override; + virtual void Invoke() override; + void pushCommand( const std::vector<OString> &rCommand ); + static void executeCommand( const std::vector<OString> &aCommand ); + +private: + Transmitter *pTransmitter; +}; + +} diff --git a/sd/source/ui/remotecontrol/Server.cxx b/sd/source/ui/remotecontrol/Server.cxx new file mode 100644 index 000000000..53bf0352c --- /dev/null +++ b/sd/source/ui/remotecontrol/Server.cxx @@ -0,0 +1,373 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <algorithm> +#include <vector> + +#include <officecfg/Office/Impress.hxx> + +#include <com/sun/star/container/XNameAccess.hpp> +#include <com/sun/star/container/XNameContainer.hpp> +#include <com/sun/star/uno/Sequence.hxx> +#include <com/sun/star/lang/XSingleServiceFactory.hpp> + +#include <comphelper/processfactory.hxx> +#include <comphelper/configuration.hxx> +#include <comphelper/sequence.hxx> +#include <sal/log.hxx> +#include <vcl/svapp.hxx> +#include <osl/socket.hxx> + +#include <sddll.hxx> + +#include "DiscoveryService.hxx" +#include "Listener.hxx" +#include <RemoteServer.hxx> +#include "BluetoothServer.hxx" +#include "Communicator.hxx" +#include "BufferedStreamSocket.hxx" + +using namespace sd; +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::beans; +using namespace ::com::sun::star::container; +using namespace ::com::sun::star::lang; +using namespace ::osl; +using namespace ::comphelper; + +namespace sd { + /** + * Used to keep track of clients that have attempted to connect, but haven't + * yet been approved. + */ + struct ClientInfoInternal: + ClientInfo + { + BufferedStreamSocket *mpStreamSocket; + OUString mPin; + + ClientInfoInternal( const OUString& rName, + BufferedStreamSocket *pSocket, + const OUString& rPin ): + ClientInfo( rName, false ), + mpStreamSocket( pSocket ), + mPin( rPin ) {} + }; +} + +RemoteServer::RemoteServer() : + Thread( "RemoteServerThread" ) +{ + SAL_INFO( "sdremote", "Instantiated RemoteServer" ); +} + +RemoteServer::~RemoteServer() +{ +} + +void RemoteServer::execute() +{ + SAL_INFO( "sdremote", "RemoteServer::execute called" ); + osl::SocketAddr aAddr( "0.0.0.0", PORT ); + if ( !mSocket.bind( aAddr ) ) + { + SAL_WARN( "sdremote", "bind failed" << mSocket.getErrorAsString() ); + spServer = nullptr; + return; + } + + if ( !mSocket.listen(3) ) + { + SAL_WARN( "sdremote", "listen failed" << mSocket.getErrorAsString() ); + spServer = nullptr; + return; + } + while ( true ) + { + StreamSocket aSocket; + SAL_INFO( "sdremote", "waiting on accept" ); + if ( mSocket.acceptConnection( aSocket ) == osl_Socket_Error ) + { + SAL_WARN( "sdremote", "accept failed" << mSocket.getErrorAsString() ); + spServer = nullptr; + return; // Closed, or other issue. + } + BufferedStreamSocket *pSocket = new BufferedStreamSocket( aSocket); + handleAcceptedConnection( pSocket ); + } + SAL_INFO( "sdremote", "shutting down RemoteServer" ); + spServer = nullptr; // Object is destroyed when Thread::execute() ends. +} + +void RemoteServer::handleAcceptedConnection( BufferedStreamSocket *pSocket ) +{ + OString aLine; + if ( ! ( pSocket->readLine( aLine) + && aLine == "LO_SERVER_CLIENT_PAIR" + && pSocket->readLine( aLine ) ) ) + { + SAL_INFO( "sdremote", "client failed to send LO_SERVER_CLIENT_PAIR, ignoring" ); + delete pSocket; + return; + } + + OString aName( aLine ); + + if ( ! pSocket->readLine( aLine ) ) + { + delete pSocket; + return; + } + OString aPin( aLine ); + + SocketAddr aClientAddr; + pSocket->getPeerAddr( aClientAddr ); + + do + { + // Read off any additional non-empty lines + // We know that we at least have the empty termination line to read. + if ( ! pSocket->readLine( aLine ) ) { + delete pSocket; + return; + } + } + while ( aLine.getLength() > 0 ); + + MutexGuard aGuard( sDataMutex ); + std::shared_ptr< ClientInfoInternal > pClient = + std::make_shared<ClientInfoInternal>( + OStringToOUString( aName, RTL_TEXTENCODING_UTF8 ), + pSocket, OStringToOUString( aPin, RTL_TEXTENCODING_UTF8 ) ); + mAvailableClients.push_back( pClient ); + + // Check if we already have this server. + Reference< XNameAccess > const xConfig = officecfg::Office::Impress::Misc::AuthorisedRemotes::get(); + const Sequence< OUString > aNames = xConfig->getElementNames(); + for ( const auto& rName : aNames ) + { + if ( rName == pClient->mName ) + { + Reference<XNameAccess> xSetItem( xConfig->getByName(rName), UNO_QUERY ); + Any axPin(xSetItem->getByName("PIN")); + OUString sPin; + axPin >>= sPin; + + if ( sPin == pClient->mPin ) { + SAL_INFO( "sdremote", "client found on validated list -- connecting" ); + connectClient( pClient, sPin ); + return; + } + } + } + + // Pin not found so inform the client. + SAL_INFO( "sdremote", "client not found on validated list" ); + pSocket->write( "LO_SERVER_VALIDATING_PIN\n\n", + strlen( "LO_SERVER_VALIDATING_PIN\n\n" ) ); +} + +RemoteServer *sd::RemoteServer::spServer = nullptr; +::osl::Mutex sd::RemoteServer::sDataMutex; +::std::vector<Communicator*> sd::RemoteServer::sCommunicators; + +void RemoteServer::setup() +{ + if (spServer) + return; + + spServer = new RemoteServer(); + spServer->launch(); + +#ifdef ENABLE_SDREMOTE_BLUETOOTH + sd::BluetoothServer::setup( &sCommunicators ); +#endif +} + +void RemoteServer::presentationStarted( const css::uno::Reference< + css::presentation::XSlideShowController > &rController ) +{ + if ( !spServer ) + return; + MutexGuard aGuard( sDataMutex ); + for ( const auto& rpCommunicator : sCommunicators ) + { + rpCommunicator->presentationStarted( rController ); + } +} +void RemoteServer::presentationStopped() +{ + if ( !spServer ) + return; + MutexGuard aGuard( sDataMutex ); + for ( const auto& rpCommunicator : sCommunicators ) + { + rpCommunicator->disposeListener(); + } +} + +void RemoteServer::removeCommunicator( Communicator const * mCommunicator ) +{ + if ( !spServer ) + return; + MutexGuard aGuard( sDataMutex ); + auto aIt = std::find(sCommunicators.begin(), sCommunicators.end(), mCommunicator); + if (aIt != sCommunicators.end()) + sCommunicators.erase( aIt ); +} + +std::vector< std::shared_ptr< ClientInfo > > RemoteServer::getClients() +{ + SAL_INFO( "sdremote", "RemoteServer::getClients() called" ); + std::vector< std::shared_ptr< ClientInfo > > aClients; + if ( spServer ) + { + MutexGuard aGuard( sDataMutex ); + aClients.assign( spServer->mAvailableClients.begin(), + spServer->mAvailableClients.end() ); + } + else + { + SAL_INFO( "sdremote", "No remote server instance => no remote clients" ); + } + // We also need to provide authorised clients (no matter whether or not + // they are actually available), so that they can be de-authorised if + // necessary. We specifically want these to be at the end of the list + // since the user is more likely to be trying to connect a new remote + // than removing an existing remote. + // We can also be sure that pre-authorised clients will not be on the + // available clients list, as they get automatically connected if seen. + // TODO: we should probably add some sort of extra labelling to mark + // authorised AND connected client. + Reference< XNameAccess > const xConfig = officecfg::Office::Impress::Misc::AuthorisedRemotes::get(); + const Sequence< OUString > aNames = xConfig->getElementNames(); + std::transform(aNames.begin(), aNames.end(), std::back_inserter(aClients), + [](const OUString& rName) -> std::shared_ptr<ClientInfo> { + return std::make_shared<ClientInfo>(rName, true); }); + + return aClients; +} + +bool RemoteServer::connectClient( const std::shared_ptr< ClientInfo >& pClient, std::u16string_view aPin ) +{ + SAL_INFO( "sdremote", "RemoteServer::connectClient called" ); + if ( !spServer ) + return false; + + ClientInfoInternal* apClient = dynamic_cast< ClientInfoInternal* >( pClient.get() ); + if ( !apClient ) + // could happen if we try to "connect" an already authorised client + { + return false; + } + + if ( apClient->mPin == aPin ) + { + // Save in settings first + std::shared_ptr< ConfigurationChanges > aChanges = ConfigurationChanges::create(); + Reference< XNameContainer > const xConfig = officecfg::Office::Impress::Misc::AuthorisedRemotes::get( aChanges ); + + Reference<XSingleServiceFactory> xChildFactory ( + xConfig, UNO_QUERY); + Reference<XNameReplace> xChild( xChildFactory->createInstance(), UNO_QUERY); + Any aValue; + if (xChild.is()) + { + // Check whether the client is already saved + Sequence< OUString > aNames = xConfig->getElementNames(); + if (comphelper::findValue(aNames, apClient->mName) != -1) + xConfig->replaceByName( apClient->mName, Any( xChild ) ); + else + xConfig->insertByName( apClient->mName, Any( xChild ) ); + aValue <<= apClient->mPin; + xChild->replaceByName("PIN", aValue); + aChanges->commit(); + } + + Communicator* pCommunicator = new Communicator( std::unique_ptr<IBluetoothSocket>(apClient->mpStreamSocket) ); + MutexGuard aGuard( sDataMutex ); + + sCommunicators.push_back( pCommunicator ); + + auto aIt = std::find(spServer->mAvailableClients.begin(), spServer->mAvailableClients.end(), pClient); + if (aIt != spServer->mAvailableClients.end()) + spServer->mAvailableClients.erase( aIt ); + pCommunicator->launch(); + return true; + } + else + { + return false; + } +} + +void RemoteServer::deauthoriseClient( const std::shared_ptr< ClientInfo >& pClient ) +{ + // TODO: we probably want to forcefully disconnect at this point too? + // But possibly via a separate function to allow just disconnecting from + // the UI. + + SAL_INFO( "sdremote", "RemoteServer::deauthoriseClient called" ); + + if ( !pClient->mbIsAlreadyAuthorised ) + // We can't remove unauthorised clients from the authorised list... + { + return; + } + + std::shared_ptr< ConfigurationChanges > aChanges = ConfigurationChanges::create(); + Reference< XNameContainer > const xConfig = + officecfg::Office::Impress::Misc::AuthorisedRemotes::get( aChanges ); + + xConfig->removeByName( pClient->mName ); + aChanges->commit(); +} + +void SdDLL::RegisterRemotes() +{ + SAL_INFO( "sdremote", "SdDLL::RegisterRemotes called" ); + + // The remote server is likely of no use in headless mode. And as only + // one instance of the server can actually own the appropriate ports its + // probably best to not even try to do so from our headless instance + // (i.e. as to avoid blocking expected usage). + // It could perhaps be argued that we would still need the remote + // server for tiled rendering of presentations, but even then this + // implementation would not be of much use, i.e. would be controlling + // the purely imaginary headless presentation -- instead we'd need + // to have some sort of mechanism of plugging in our tiled rendering + // client to be controlled by the remote server, or provide an + // alternative implementation. + if ( Application::IsHeadlessModeEnabled() ) + return; + + if ( !officecfg::Office::Impress::Misc::Start::EnableSdremote::get() ) + return; + + sd::RemoteServer::setup(); + sd::DiscoveryService::setup(); +} + +void RemoteServer::ensureDiscoverable() +{ + // FIXME: we could also enable listening on our WiFi + // socket here to significantly reduce the attack surface. +#ifdef ENABLE_SDREMOTE_BLUETOOTH + BluetoothServer::ensureDiscoverable(); +#endif +} + +void RemoteServer::restoreDiscoverable() +{ +#ifdef ENABLE_SDREMOTE_BLUETOOTH + BluetoothServer::restoreDiscoverable(); +#endif +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Transmitter.cxx b/sd/source/ui/remotecontrol/Transmitter.cxx new file mode 100644 index 000000000..cca6a3bee --- /dev/null +++ b/sd/source/ui/remotecontrol/Transmitter.cxx @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#include "Transmitter.hxx" +#include "IBluetoothSocket.hxx" +#include <sal/log.hxx> + +using namespace osl; // Sockets etc. +using namespace sd; + +Transmitter::Transmitter( IBluetoothSocket* aSocket ) + : pStreamSocket( aSocket ), + mFinishRequested( false ) +{ +} + +void SAL_CALL Transmitter::run() +{ + osl_setThreadName("bluetooth Transmitter"); + + while ( true ) + { + mProcessingRequired.wait(); + + ::osl::MutexGuard aGuard( mMutex ); + + if ( mFinishRequested ) { + return; + } + if ( !mHighPriority.empty() ) + { + OString aMessage( mHighPriority.front() ); + mHighPriority.pop(); + SAL_INFO( "sdremote.bluetooth", "write high prio line '" << aMessage << "'" ); + pStreamSocket->write( aMessage.getStr(), aMessage.getLength() ); + } + else if ( !mLowPriority.empty() ) + { + OString aMessage( mLowPriority.front() ); + mLowPriority.pop(); + SAL_INFO( "sdremote.bluetooth", "write normal line '" << aMessage << "'" ); + pStreamSocket->write( aMessage.getStr(), aMessage.getLength() ); + } + + if ( mLowPriority.empty() && mHighPriority.empty()) + { + mProcessingRequired.reset(); + } + } +} + +void Transmitter::notifyFinished() +{ + ::osl::MutexGuard aGuard( mMutex ); + mFinishRequested = true; + mProcessingRequired.set(); +} + +Transmitter::~Transmitter() +{ +} + +void Transmitter::addMessage( const OString& aMessage, const Priority aPriority ) +{ + ::osl::MutexGuard aGuard( mMutex ); + switch ( aPriority ) + { + case PRIORITY_LOW: + mLowPriority.push( aMessage ); + break; + case PRIORITY_HIGH: + mHighPriority.push( aMessage ); + break; + } + if ( !mProcessingRequired.check() ) + { + mProcessingRequired.set(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/Transmitter.hxx b/sd/source/ui/remotecontrol/Transmitter.hxx new file mode 100644 index 000000000..c24f5a5a4 --- /dev/null +++ b/sd/source/ui/remotecontrol/Transmitter.hxx @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include <osl/conditn.hxx> +#include <osl/mutex.hxx> +#include <osl/thread.hxx> +#include <rtl/string.hxx> + +#include <queue> + +namespace sd { struct IBluetoothSocket; } + +namespace sd +{ + +class Transmitter +: public osl::Thread +{ +public: + enum Priority { PRIORITY_LOW = 1, PRIORITY_HIGH }; + explicit Transmitter( ::sd::IBluetoothSocket* aSocket ); + virtual ~Transmitter() override; + void addMessage( const OString& aMessage, const Priority aPriority ); + void notifyFinished(); + +private: + virtual void SAL_CALL run() override; + + ::sd::IBluetoothSocket* pStreamSocket; + + ::osl::Condition mProcessingRequired; + + ::osl::Mutex mMutex; + /** + * Used to indicate that we're done and the transmitter loop should exit. + * All access must be guarded my `mMutex`. + */ + bool mFinishRequested; + /// Queue for low priority messages. All access must be guarded my `mMutex`. + std::queue<OString> mLowPriority; + /// Queue for high priority messages. All access must be guarded my `mMutex`. + std::queue<OString> mHighPriority; +}; + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/sd/source/ui/remotecontrol/WINNetworkService.cxx b/sd/source/ui/remotecontrol/WINNetworkService.cxx new file mode 100644 index 000000000..bd2decf62 --- /dev/null +++ b/sd/source/ui/remotecontrol/WINNetworkService.cxx @@ -0,0 +1,19 @@ +#include <string> +#include <iostream> +#include "WINNetworkService.hxx" +#include <sal/log.hxx> + +void sd::WINNetworkService::setup() +{ + DNSServiceErrorType err = DNSServiceRegister(&client, 0, 0, nullptr, kREG_TYPE, "local", nullptr, 1599, 1, "", nullptr, this ); + + if (kDNSServiceErr_NoError != err) + SAL_WARN("sdremote.wifi", "DNSServiceRegister failed: " << err); + + // Fail silently +} + +void sd::WINNetworkService::clear() +{ + DNSServiceRefDeallocate(client); +} diff --git a/sd/source/ui/remotecontrol/WINNetworkService.hxx b/sd/source/ui/remotecontrol/WINNetworkService.hxx new file mode 100644 index 000000000..3d096dc0f --- /dev/null +++ b/sd/source/ui/remotecontrol/WINNetworkService.hxx @@ -0,0 +1,23 @@ +#pragma once + +#include <string> +#undef WB_LEFT +#undef WB_RIGHT +#include <dns_sd.h> +#include "ZeroconfService.hxx" + +namespace sd{ + class WINNetworkService : public ZeroconfService + { + private: + DNSServiceRef client; + + public: + WINNetworkService(const std::string& aname = "", unsigned int aport = 1599) + : ZeroconfService(aname, aport), client(nullptr) {} + + void clear() override; + void setup() override; + + }; +} diff --git a/sd/source/ui/remotecontrol/ZeroconfService.hxx b/sd/source/ui/remotecontrol/ZeroconfService.hxx new file mode 100644 index 000000000..a595d0b58 --- /dev/null +++ b/sd/source/ui/remotecontrol/ZeroconfService.hxx @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef ZEROCONF_SERVICE +#define ZEROCONF_SERVICE + +#include <string> + +/** +* The port used by LO's custom remote server discovery protocol. +*/ +#define PORT_DISCOVERY 1598 +#define BUFFER_SIZE 200 + +#define kREG_TYPE "_impressremote._tcp" + +struct sockaddr_in; + +typedef unsigned int uint; + +namespace sd{ + + class ZeroconfService + { + protected: + std::string name; + uint port; + + public: + explicit ZeroconfService(const std::string& aname, uint aport) + :name(aname), port(aport){} + virtual ~ZeroconfService(){} + + const std::string& getName() const {return name;} + void setName(const char * n) {name = n;} + + // Clean up the service when closing + virtual void clear() = 0; + // Bonjour for OSX, Avahi for Linux + virtual void setup() = 0; + }; + +} +#endif |