diff options
Diffstat (limited to 'tests/manual/examples')
-rw-r--r-- | tests/manual/examples/btscanner/CMakeLists.txt | 48 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/Info.plist | 41 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/btscanner.pro | 24 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/device.cpp | 182 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/device.h | 49 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/device.ui | 118 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/main.cpp | 33 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/service.cpp | 62 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/service.h | 36 | ||||
-rw-r--r-- | tests/manual/examples/btscanner/service.ui | 71 |
10 files changed, 664 insertions, 0 deletions
diff --git a/tests/manual/examples/btscanner/CMakeLists.txt b/tests/manual/examples/btscanner/CMakeLists.txt new file mode 100644 index 00000000..08bac2df --- /dev/null +++ b/tests/manual/examples/btscanner/CMakeLists.txt @@ -0,0 +1,48 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +if (NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(btscanner LANGUAGES CXX) + find_package(Qt6BuildInternals COMPONENTS STANDALONE_TEST) + endif() + +qt_internal_add_manual_test(btscanner + GUI + SOURCES + device.cpp device.h device.ui + main.cpp + service.cpp service.h service.ui + LIBRARIES + Qt::Bluetooth + Qt::Core + Qt::Widgets + ENABLE_AUTOGEN_TOOLS + uic +) + +if(MACOS) + # Explicitly link against the static permission plugin because tests + # currently don't have finalizers run for them except for iOS. + # TODO: Remove this when qtbase automatically runs finalizers for tests: QTBUG-112212 + target_link_libraries(btscanner PRIVATE Qt6::QDarwinBluetoothPermissionPlugin) +endif() + +set_target_properties(btscanner PROPERTIES + WIN32_EXECUTABLE TRUE + MACOSX_BUNDLE TRUE +) + +if(APPLE) + if (IOS) + set_target_properties(btscanner PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist" + ) + else() + # Using absolute path for shared plist files is a Ninja bug workaround + get_filename_component(SHARED_PLIST_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../examples/bluetooth/shared ABSOLUTE) + set_target_properties(btscanner PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${SHARED_PLIST_DIR}/Info.cmake.macos.plist" + ) + endif() +endif() diff --git a/tests/manual/examples/btscanner/Info.plist b/tests/manual/examples/btscanner/Info.plist new file mode 100644 index 00000000..49fd2191 --- /dev/null +++ b/tests/manual/examples/btscanner/Info.plist @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDisplayName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string>${ASSETCATALOG_COMPILER_APPICON_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>${QMAKE_SHORT_VERSION}</string> + <key>CFBundleSignature</key> + <string>${QMAKE_PKGINFO_TYPEINFO}</string> + <key>CFBundleVersion</key> + <string>${QMAKE_FULL_VERSION}</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>MinimumOSVersion</key> + <string>${IPHONEOS_DEPLOYMENT_TARGET}</string> + <key>NSBluetoothAlwaysUsageDescription</key> + <string>Qt's BT scanner wants to access your Bluetooth adapter!</string> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/tests/manual/examples/btscanner/btscanner.pro b/tests/manual/examples/btscanner/btscanner.pro new file mode 100644 index 00000000..3ef49969 --- /dev/null +++ b/tests/manual/examples/btscanner/btscanner.pro @@ -0,0 +1,24 @@ +TARGET = btscanner + +QT = core bluetooth widgets +requires(qtConfig(listwidget)) +TEMPLATE = app + +SOURCES = \ + main.cpp \ + device.cpp \ + service.cpp + +ios: QMAKE_INFO_PLIST = Info.plist +macos: QMAKE_INFO_PLIST = ../../../../examples/bluetooth/shared/Info.qmake.macos.plist + +HEADERS = \ + device.h \ + service.h + +FORMS = \ + device.ui \ + service.ui + +target.path = $$[QT_INSTALL_EXAMPLES]/bluetooth/btscanner +INSTALLS += target diff --git a/tests/manual/examples/btscanner/device.cpp b/tests/manual/examples/btscanner/device.cpp new file mode 100644 index 00000000..6d1f775f --- /dev/null +++ b/tests/manual/examples/btscanner/device.cpp @@ -0,0 +1,182 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "device.h" +#include "service.h" +#include "ui_device.h" + +#include <QtBluetooth/qbluetoothaddress.h> +#include <QtBluetooth/qbluetoothdevicediscoveryagent.h> +#include <QtBluetooth/qbluetoothlocaldevice.h> + +#include <QtWidgets/qmenu.h> + +#if QT_CONFIG(permissions) +#include <QtCore/qpermissions.h> +#include <QtWidgets/qmessagebox.h> +#include <QtWidgets/qapplication.h> +#endif // QT_CONFIG(permissions) + +static QColor colorForPairing(QBluetoothLocalDevice::Pairing pairing) +{ + return pairing == QBluetoothLocalDevice::Paired + || pairing == QBluetoothLocalDevice::AuthorizedPaired + ? QColor(Qt::green) : QColor(Qt::red); +} + +DeviceDiscoveryDialog::DeviceDiscoveryDialog(QWidget *parent) : + QDialog(parent), + localDevice(new QBluetoothLocalDevice(this)), + ui(new Ui::DeviceDiscovery) +{ +#ifdef Q_OS_ANDROID + this->setWindowState(Qt::WindowMaximized); +#endif + ui->setupUi(this); + ui->stopScan->setVisible(false); + + // In case of multiple Bluetooth adapters it is possible to set the adapter + // to be used. Example code: + // + // QBluetoothAddress address("XX:XX:XX:XX:XX:XX"); + // discoveryAgent = new QBluetoothDeviceDiscoveryAgent(address, this); + + discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); + + connect(ui->scan, &QAbstractButton::clicked, this, &DeviceDiscoveryDialog::startScan); + connect(ui->stopScan, &QAbstractButton::clicked, this, &DeviceDiscoveryDialog::stopScan); + + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &DeviceDiscoveryDialog::addDevice); + connect(discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, + this, &DeviceDiscoveryDialog::scanFinished); + + connect(ui->list, &QListWidget::itemActivated, + this, &DeviceDiscoveryDialog::itemActivated); + + connect(localDevice, &QBluetoothLocalDevice::hostModeStateChanged, + this, &DeviceDiscoveryDialog::hostModeStateChanged); + + hostModeStateChanged(localDevice->hostMode()); + // add context menu for devices to be able to pair device + ui->list->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->list, &QWidget::customContextMenuRequested, + this, &DeviceDiscoveryDialog::displayPairingMenu); + connect(localDevice, &QBluetoothLocalDevice::pairingFinished, + this, &DeviceDiscoveryDialog::pairingDone); +} + +DeviceDiscoveryDialog::~DeviceDiscoveryDialog() +{ + delete ui; +} + +void DeviceDiscoveryDialog::addDevice(const QBluetoothDeviceInfo &info) +{ + const QString label = info.address().toString() + u' ' + info.name(); + const auto items = ui->list->findItems(label, Qt::MatchExactly); + if (items.isEmpty()) { + QListWidgetItem *item = new QListWidgetItem(label); + QBluetoothLocalDevice::Pairing pairingStatus = localDevice->pairingStatus(info.address()); + item->setForeground(colorForPairing(pairingStatus)); + ui->list->addItem(item); + } +} + +void DeviceDiscoveryDialog::startScan() +{ +#if QT_CONFIG(permissions) + if (qApp->checkPermission(QBluetoothPermission{}) != Qt::PermissionStatus::Granted) { + QMessageBox::warning(this, tr("Missing permission"), + tr("Permission is needed to use Bluetooth. "\ + "Please grant the permission to this "\ + "application in the system settings.")); + return; + } +#endif // QT_CONFIG(permissions) + discoveryAgent->start(); + ui->scan->setVisible(false); + ui->stopScan->setVisible(true); +} + +void DeviceDiscoveryDialog::stopScan() +{ + discoveryAgent->stop(); + scanFinished(); +} + +void DeviceDiscoveryDialog::scanFinished() +{ + ui->scan->setVisible(true); + ui->stopScan->setVisible(false); +} + +void DeviceDiscoveryDialog::itemActivated(QListWidgetItem *item) +{ + const QString text = item->text(); + const auto index = text.indexOf(' '); + if (index == -1) + return; + + QBluetoothAddress address(text.left(index)); + QString name(text.mid(index + 1)); + + ServiceDiscoveryDialog d(name, address); + d.exec(); +} + +void DeviceDiscoveryDialog::on_discoverable_clicked(bool clicked) +{ + if (clicked) + localDevice->setHostMode(QBluetoothLocalDevice::HostDiscoverable); + else + localDevice->setHostMode(QBluetoothLocalDevice::HostConnectable); +} + +void DeviceDiscoveryDialog::on_power_clicked(bool clicked) +{ + if (clicked) + localDevice->powerOn(); + else + localDevice->setHostMode(QBluetoothLocalDevice::HostPoweredOff); +} + +void DeviceDiscoveryDialog::hostModeStateChanged(QBluetoothLocalDevice::HostMode mode) +{ + ui->power->setChecked(mode != QBluetoothLocalDevice::HostPoweredOff); + ui->discoverable->setChecked(mode == QBluetoothLocalDevice::HostDiscoverable); + + const bool on = mode != QBluetoothLocalDevice::HostPoweredOff; + ui->scan->setEnabled(on); + ui->discoverable->setEnabled(on); +} +void DeviceDiscoveryDialog::displayPairingMenu(const QPoint &pos) +{ + if (ui->list->count() == 0) + return; + QMenu menu(this); + QAction *pairAction = menu.addAction("Pair"); + QAction *removePairAction = menu.addAction("Remove Pairing"); + QAction *chosenAction = menu.exec(ui->list->viewport()->mapToGlobal(pos)); + QListWidgetItem *currentItem = ui->list->currentItem(); + + QString text = currentItem->text(); + const auto index = text.indexOf(' '); + if (index == -1) + return; + + QBluetoothAddress address (text.left(index)); + if (chosenAction == pairAction) { + localDevice->requestPairing(address, QBluetoothLocalDevice::Paired); + } else if (chosenAction == removePairAction) { + localDevice->requestPairing(address, QBluetoothLocalDevice::Unpaired); + } +} +void DeviceDiscoveryDialog::pairingDone(const QBluetoothAddress &address, + QBluetoothLocalDevice::Pairing pairing) +{ + const auto items = ui->list->findItems(address.toString(), Qt::MatchContains); + const QColor color = colorForPairing(pairing); + for (auto *item : items) + item->setForeground(color); +} diff --git a/tests/manual/examples/btscanner/device.h b/tests/manual/examples/btscanner/device.h new file mode 100644 index 00000000..389062bb --- /dev/null +++ b/tests/manual/examples/btscanner/device.h @@ -0,0 +1,49 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef DEVICE_H +#define DEVICE_H + +#include <QtBluetooth/qbluetoothlocaldevice.h> + +#include <QtWidgets/qdialog.h> + +QT_BEGIN_NAMESPACE +class QBluetoothAddress; +class QBluetoothDeviceDiscoveryAgent; +class QBluetoothDeviceInfo; +class QListWidgetItem; + +namespace Ui { + class DeviceDiscovery; +} +QT_END_NAMESPACE + +class DeviceDiscoveryDialog : public QDialog +{ + Q_OBJECT + +public: + DeviceDiscoveryDialog(QWidget *parent = nullptr); + ~DeviceDiscoveryDialog(); + +public slots: + void addDevice(const QBluetoothDeviceInfo &info); + void on_power_clicked(bool clicked); + void on_discoverable_clicked(bool clicked); + void displayPairingMenu(const QPoint &pos); + void pairingDone(const QBluetoothAddress &address, QBluetoothLocalDevice::Pairing pairing); +private slots: + void startScan(); + void stopScan(); + void scanFinished(); + void itemActivated(QListWidgetItem *item); + void hostModeStateChanged(QBluetoothLocalDevice::HostMode mode); + +private: + QBluetoothDeviceDiscoveryAgent *discoveryAgent; + QBluetoothLocalDevice *localDevice; + Ui::DeviceDiscovery *ui; +}; + +#endif diff --git a/tests/manual/examples/btscanner/device.ui b/tests/manual/examples/btscanner/device.ui new file mode 100644 index 00000000..fd86a358 --- /dev/null +++ b/tests/manual/examples/btscanner/device.ui @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DeviceDiscovery</class> + <widget class="QDialog" name="DeviceDiscovery"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>411</height> + </rect> + </property> + <property name="windowTitle"> + <string>Bluetooth Scanner</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListWidget" name="list"/> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Local Device</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QCheckBox" name="power"> + <property name="text"> + <string>Bluetooth Powered On</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="discoverable"> + <property name="text"> + <string>Discoverable</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="scan"> + <property name="text"> + <string>Scan</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="stopScan"> + <property name="text"> + <string>Stop scan</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="clear"> + <property name="text"> + <string>Clear</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="quit"> + <property name="text"> + <string>Quit</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>quit</sender> + <signal>clicked()</signal> + <receiver>DeviceDiscovery</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>323</x> + <y>275</y> + </hint> + <hint type="destinationlabel"> + <x>396</x> + <y>268</y> + </hint> + </hints> + </connection> + <connection> + <sender>clear</sender> + <signal>clicked()</signal> + <receiver>list</receiver> + <slot>clear()</slot> + <hints> + <hint type="sourcelabel"> + <x>188</x> + <y>276</y> + </hint> + <hint type="destinationlabel"> + <x>209</x> + <y>172</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/tests/manual/examples/btscanner/main.cpp b/tests/manual/examples/btscanner/main.cpp new file mode 100644 index 00000000..8ed2e652 --- /dev/null +++ b/tests/manual/examples/btscanner/main.cpp @@ -0,0 +1,33 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "device.h" + +#include <QtCore/qloggingcategory.h> +#include <QtWidgets/qapplication.h> + +#if QT_CONFIG(permissions) +#include <QtCore/qpermissions.h> +#endif + +int main(int argc, char *argv[]) +{ + // QLoggingCategory::setFilterRules(QStringLiteral("qt.bluetooth* = true")); + QApplication app(argc, argv); + DeviceDiscoveryDialog d; + + d.show(); + + // Check, and if needed, request a permission to use Bluetooth. +#if QT_CONFIG(permissions) + const auto permissionStatus = app.checkPermission(QBluetoothPermission{}); + if (permissionStatus == Qt::PermissionStatus::Undetermined) { + app.requestPermission(QBluetoothPermission{}, [](const QPermission &){ + }); + } + // Else means either 'Granted' or 'Denied' and both normally must be + // changed using the system's settings application. +#endif // QT_CONFIG(permissions) + + return app.exec(); +} diff --git a/tests/manual/examples/btscanner/service.cpp b/tests/manual/examples/btscanner/service.cpp new file mode 100644 index 00000000..aa6fe43c --- /dev/null +++ b/tests/manual/examples/btscanner/service.cpp @@ -0,0 +1,62 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2013 BlackBerry Limited. All rights reserved. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "service.h" +#include "ui_service.h" + +#include <QtBluetooth/qbluetoothaddress.h> +#include <QtBluetooth/qbluetoothlocaldevice.h> +#include <QtBluetooth/qbluetoothservicediscoveryagent.h> +#include <QtBluetooth/qbluetoothserviceinfo.h> +#include <QtBluetooth/qbluetoothuuid.h> + + +ServiceDiscoveryDialog::ServiceDiscoveryDialog(const QString &name, + const QBluetoothAddress &address, QWidget *parent) + : QDialog(parent), ui(new Ui::ServiceDiscovery) +{ + ui->setupUi(this); + + //Using default Bluetooth adapter + QBluetoothLocalDevice localDevice; + QBluetoothAddress adapterAddress = localDevice.address(); + + // In case of multiple Bluetooth adapters it is possible to + // set which adapter will be used by providing MAC Address. + // Example code: + // + // QBluetoothAddress adapterAddress("XX:XX:XX:XX:XX:XX"); + // discoveryAgent = new QBluetoothServiceDiscoveryAgent(adapterAddress, this); + + discoveryAgent = new QBluetoothServiceDiscoveryAgent(adapterAddress, this); + discoveryAgent->setRemoteAddress(address); + + setWindowTitle(name); + + connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::serviceDiscovered, + this, &ServiceDiscoveryDialog::addService); + connect(discoveryAgent, &QBluetoothServiceDiscoveryAgent::finished, + ui->status, &QWidget::hide); + + discoveryAgent->start(); +} + +ServiceDiscoveryDialog::~ServiceDiscoveryDialog() +{ + delete ui; +} + +void ServiceDiscoveryDialog::addService(const QBluetoothServiceInfo &info) +{ + if (info.serviceName().isEmpty()) + return; + + QString line = info.serviceName(); + if (!info.serviceDescription().isEmpty()) + line.append("\n\t" + info.serviceDescription()); + if (!info.serviceProvider().isEmpty()) + line.append("\n\t" + info.serviceProvider()); + + ui->list->addItem(line); +} diff --git a/tests/manual/examples/btscanner/service.h b/tests/manual/examples/btscanner/service.h new file mode 100644 index 00000000..1c84f38c --- /dev/null +++ b/tests/manual/examples/btscanner/service.h @@ -0,0 +1,36 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef SERVICE_H +#define SERVICE_H + +#include <QtWidgets/qdialog.h> + +QT_BEGIN_NAMESPACE +class QBluetoothAddress; +class QBluetoothServiceDiscoveryAgent; +class QBluetoothServiceInfo; + +namespace Ui { + class ServiceDiscovery; +} +QT_END_NAMESPACE + +class ServiceDiscoveryDialog : public QDialog +{ + Q_OBJECT + +public: + ServiceDiscoveryDialog(const QString &name, const QBluetoothAddress &address, + QWidget *parent = nullptr); + ~ServiceDiscoveryDialog(); + +public slots: + void addService(const QBluetoothServiceInfo &info); + +private: + QBluetoothServiceDiscoveryAgent *discoveryAgent; + Ui::ServiceDiscovery *ui; +}; + +#endif diff --git a/tests/manual/examples/btscanner/service.ui b/tests/manual/examples/btscanner/service.ui new file mode 100644 index 00000000..4ca12ee0 --- /dev/null +++ b/tests/manual/examples/btscanner/service.ui @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ServiceDiscovery</class> + <widget class="QDialog" name="ServiceDiscovery"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>539</width> + <height>486</height> + </rect> + </property> + <property name="windowTitle"> + <string>Available Services</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QListWidget" name="list"/> + </item> + <item> + <widget class="QLabel" name="status"> + <property name="text"> + <string>Querying...</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>ServiceDiscovery</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>396</x> + <y>457</y> + </hint> + <hint type="destinationlabel"> + <x>535</x> + <y>443</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>ServiceDiscovery</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>339</x> + <y>464</y> + </hint> + <hint type="destinationlabel"> + <x>535</x> + <y>368</y> + </hint> + </hints> + </connection> + </connections> +</ui> |