diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3c0fbe1e..1eabcbf1 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -547,4 +547,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
Plugin stats
+ Enable UDP device discovery
+
diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java
index 569176f1..c72c0c22 100644
--- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java
+++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLinkProvider.java
@@ -25,6 +25,7 @@ import org.kde.kdeconnect.Helpers.TrustedNetworkHelper;
import org.kde.kdeconnect.KdeConnect;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
+import org.kde.kdeconnect.UserInterface.SettingsFragment;
import java.io.BufferedReader;
import java.io.IOException;
@@ -58,9 +59,9 @@ import kotlin.text.Charsets;
*/
public class LanLinkProvider extends BaseLinkProvider {
- private final static int UDP_PORT = 1716;
- private final static int MIN_PORT = 1716;
- private final static int MAX_PORT = 1764;
+ final static int UDP_PORT = 1716;
+ final static int MIN_PORT = 1716;
+ final static int MAX_PORT = 1764;
final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
final static int MAX_UDP_PACKET_SIZE = 1024 * 512;
@@ -69,13 +70,15 @@ public class LanLinkProvider extends BaseLinkProvider {
private final Context context;
- private final HashMap visibleDevices = new HashMap<>(); //Links by device id
+ final HashMap visibleDevices = new HashMap<>(); //Links by device id
final ConcurrentHashMap lastConnectionTime = new ConcurrentHashMap<>();
private ServerSocket tcpServer;
private DatagramSocket udpServer;
+ private MdnsDiscovery mdnsDiscovery;
+
private long lastBroadcast = 0;
private final static long delayBetweenBroadcasts = 200;
@@ -265,6 +268,7 @@ public class LanLinkProvider extends BaseLinkProvider {
public LanLinkProvider(Context context) {
this.context = context;
+ this.mdnsDiscovery = new MdnsDiscovery(context, this);
}
private void setupUdpListener() {
@@ -352,11 +356,11 @@ public class LanLinkProvider extends BaseLinkProvider {
}
private void broadcastUdpIdentityPacket() {
- if (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) {
- Log.i("LanLinkProvider", "broadcastUdpPacket: relax cowboy");
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!preferences.getBoolean(SettingsFragment.KEY_UDP_BROADCAST_ENABLED, true)) {
+ Log.i("LanLinkProvider", "UDP broadcast is disabled in settings. Skipping.");
return;
}
- lastBroadcast = System.currentTimeMillis();
ThreadHelper.execute(() -> {
List ipStringList = CustomDevicesActivity
@@ -436,19 +440,32 @@ public class LanLinkProvider extends BaseLinkProvider {
setupUdpListener();
setupTcpListener();
+ mdnsDiscovery.startDiscovering();
+ mdnsDiscovery.startAnnouncing();
+
broadcastUdpIdentityPacket();
}
}
@Override
public void onNetworkChange() {
+ if (System.currentTimeMillis() < lastBroadcast + delayBetweenBroadcasts) {
+ Log.i("LanLinkProvider", "onNetworkChange: relax cowboy");
+ return;
+ }
+ lastBroadcast = System.currentTimeMillis();
+
broadcastUdpIdentityPacket();
+ mdnsDiscovery.stopDiscovering();
+ mdnsDiscovery.startDiscovering();
}
@Override
public void onStop() {
//Log.i("KDE/LanLinkProvider", "onStop");
listening = false;
+ mdnsDiscovery.stopAnnouncing();
+ mdnsDiscovery.stopDiscovering();
try {
tcpServer.close();
} catch (Exception e) {
diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/MdnsDiscovery.java b/src/org/kde/kdeconnect/Backends/LanBackend/MdnsDiscovery.java
new file mode 100644
index 00000000..72c5ad5a
--- /dev/null
+++ b/src/org/kde/kdeconnect/Backends/LanBackend/MdnsDiscovery.java
@@ -0,0 +1,224 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+
+
+package org.kde.kdeconnect.Backends.LanBackend;
+
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+import org.kde.kdeconnect.Helpers.DeviceHelper;
+
+import java.net.InetAddress;
+import java.util.Collections;
+
+public class MdnsDiscovery {
+
+ static final String LOG_TAG = "MdnsDiscovery";
+
+ static final String SERVICE_TYPE = "_kdeconnect._udp";
+
+ private final Context context;
+
+ private final LanLinkProvider lanLinkProvider;
+
+ private final NsdManager mNsdManager;
+ private NsdManager.RegistrationListener registrationListener;
+ private NsdManager.DiscoveryListener discoveryListener;
+
+ private WifiManager.MulticastLock multicastLock;
+
+ private NsdResolveQueue mNsdResolveQueue;
+
+ public MdnsDiscovery(Context context, LanLinkProvider lanLinkProvider) {
+ this.context = context;
+ this.lanLinkProvider = lanLinkProvider;
+ this.mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
+ this.mNsdResolveQueue = new NsdResolveQueue(this.mNsdManager);
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ multicastLock = wifiManager.createMulticastLock("kdeConnectMdnsMulticastLock");
+ }
+
+ void startDiscovering() {
+ if (discoveryListener == null) {
+ multicastLock.acquire();
+ discoveryListener = createDiscoveryListener();
+ mNsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, discoveryListener);
+ }
+ }
+
+ void stopDiscovering() {
+ try {
+ if (discoveryListener != null) {
+ mNsdManager.stopServiceDiscovery(discoveryListener);
+ multicastLock.release();
+ }
+ } catch(IllegalArgumentException e) {
+ // Ignore "listener not registered" exception
+ }
+ discoveryListener = null;
+ }
+
+ void stopAnnouncing() {
+ try {
+ if (registrationListener != null) {
+ mNsdManager.unregisterService(registrationListener);
+ }
+ } catch(IllegalArgumentException e) {
+ // Ignore "listener not registered" exception
+ }
+ registrationListener = null;
+ }
+
+ void startAnnouncing() {
+ if (registrationListener == null) {
+ NsdServiceInfo serviceInfo;
+ try {
+ serviceInfo = createNsdServiceInfo();
+ } catch (IllegalAccessException e) {
+ Log.w(LOG_TAG, "Couldn't start announcing via MDNS: " + e.getMessage());
+ return;
+ }
+ registrationListener = createRegistrationListener();
+ mNsdManager.registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener);
+ }
+ }
+
+ NsdManager.RegistrationListener createRegistrationListener() {
+ return new NsdManager.RegistrationListener() {
+
+ @Override
+ public void onServiceRegistered(NsdServiceInfo serviceInfo) {
+ // If Android changed the service name to avoid conflicts, here we can read it.
+ Log.i(LOG_TAG, "Registered " + serviceInfo.getServiceName());
+ }
+
+ @Override
+ public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Log.e(LOG_TAG, "Registration failed with: " + errorCode);
+ }
+
+ @Override
+ public void onServiceUnregistered(NsdServiceInfo serviceInfo) {
+ Log.d(LOG_TAG, "Service unregistered: " + serviceInfo);
+ }
+
+ @Override
+ public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Log.e(LOG_TAG, "Unregister of " + serviceInfo + " failed with: " + errorCode);
+ }
+ };
+ }
+
+ public NsdServiceInfo createNsdServiceInfo() throws IllegalAccessException {
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
+
+ String deviceId = DeviceHelper.getDeviceId(context);
+ // Without resolving the DNS, the service name is the only info we have so it must be sufficient to identify a device.
+ // Also, it must be unique, otherwise it will be automatically renamed. For these reasons we use the deviceId.
+ serviceInfo.setServiceName(deviceId);
+ serviceInfo.setServiceType(SERVICE_TYPE);
+ serviceInfo.setPort(LanLinkProvider.UDP_PORT);
+
+ // The following fields aren't really used for anything, since we can't include enough info
+ // for it to be useful (namely: we can't include the device certificate).
+ // Each field (key + value) needs to be < 255 bytes. All the fields combined need to be < 1300 bytes.
+ // Also, on Android Lollipop those fields aren't resolved.
+ String deviceName = DeviceHelper.getDeviceName(context);
+ String deviceType = DeviceHelper.getDeviceType(context).toString();
+ String protocolVersion = Integer.toString(DeviceHelper.ProtocolVersion);
+ serviceInfo.setAttribute("id", deviceId);
+ serviceInfo.setAttribute("name", deviceName);
+ serviceInfo.setAttribute("type", deviceType);
+ serviceInfo.setAttribute("protocol", protocolVersion);
+
+ Log.i(LOG_TAG, "My MDNS info: " + serviceInfo);
+
+ return serviceInfo;
+ }
+
+ NsdManager.DiscoveryListener createDiscoveryListener() {
+ return new NsdManager.DiscoveryListener() {
+
+ final String myId = DeviceHelper.getDeviceId(context);
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ Log.i(LOG_TAG, "Service discovery started: " + serviceType);
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ Log.d(LOG_TAG, "Service discovered: " + serviceInfo);
+
+ String deviceId = serviceInfo.getServiceName();
+
+ if (myId.equals(deviceId)) {
+ Log.d(LOG_TAG, "Discovered myself, ignoring.");
+ return;
+ }
+
+ if (lanLinkProvider.visibleDevices.containsKey(deviceId)) {
+ Log.i(LOG_TAG, "MDNS discovered " + deviceId + " to which I'm already connected to. Ignoring.");
+ return;
+ }
+
+ // We use a queue because only one service can be resolved at
+ // a time, otherwise we get error 3 (already active) in onResolveFailed.
+ mNsdResolveQueue.resolveOrEnqueue(serviceInfo, createResolveListener());
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ Log.w(LOG_TAG, "Service lost: " + serviceInfo);
+ // We can't see this device via mdns. This probably means it's not reachable anymore
+ // but we do nothing here since we have other ways to do detect unreachable devices
+ // that hopefully will also trigger.
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ Log.i(LOG_TAG, "MDNS discovery stopped: " + serviceType);
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(LOG_TAG, "MDNS discovery start failed: " + errorCode);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ Log.e(LOG_TAG, "MDNS discovery stop failed: " + errorCode);
+ }
+ };
+ }
+
+
+ /**
+ * Returns a new listener instance since NsdManager wants a different listener each time you call resolveService
+ */
+ NsdManager.ResolveListener createResolveListener() {
+ return new NsdManager.ResolveListener() {
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Log.w(LOG_TAG, "MDNS error " + errorCode + " resolving service: " + serviceInfo);
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ Log.i(LOG_TAG, "MDNS successfully resolved " + serviceInfo);
+
+ // Let the LanLinkProvider handle the connection
+ InetAddress remoteAddress = serviceInfo.getHost();
+ lanLinkProvider.sendUdpIdentityPacket(Collections.singletonList(remoteAddress));
+ }
+ };
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/NsdResolveQueue.java b/src/org/kde/kdeconnect/Backends/LanBackend/NsdResolveQueue.java
new file mode 100644
index 00000000..c61506af
--- /dev/null
+++ b/src/org/kde/kdeconnect/Backends/LanBackend/NsdResolveQueue.java
@@ -0,0 +1,91 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+
+package org.kde.kdeconnect.Backends.LanBackend;
+
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.LinkedList;
+
+public class NsdResolveQueue {
+
+ static final String LOG_TAG = "NsdResolveQueue";
+
+ final @NonNull NsdManager mNsdManager;
+
+ private final Object mLock = new Object();
+ private final LinkedList mResolveRequests = new LinkedList<>();
+
+ public NsdResolveQueue(NsdManager nsdManager) {
+ this.mNsdManager = nsdManager;
+ }
+
+ private static class PendingResolve {
+ final @NonNull NsdServiceInfo serviceInfo;
+ final @NonNull NsdManager.ResolveListener listener;
+
+ private PendingResolve(@NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
+ this.serviceInfo = serviceInfo;
+ this.listener = listener;
+ }
+ }
+
+ public void resolveOrEnqueue(@NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
+ synchronized (mLock) {
+ for (PendingResolve existing : mResolveRequests) {
+ if (serviceInfo.getServiceName().equals(existing.serviceInfo.getServiceName())) {
+ Log.i(LOG_TAG, "Not enqueuing a new resolve request for the same service: " + serviceInfo.getServiceName());
+ return;
+ }
+ }
+ mResolveRequests.addLast(new PendingResolve(serviceInfo, new ListenerWrapper(listener)));
+
+ if (mResolveRequests.size() == 1) {
+ resolveNextRequest();
+ }
+ }
+ }
+
+ private class ListenerWrapper implements NsdManager.ResolveListener {
+ private final @NonNull NsdManager.ResolveListener mListener;
+
+ private ListenerWrapper(@NonNull NsdManager.ResolveListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ mListener.onResolveFailed(serviceInfo, errorCode);
+
+ synchronized (mLock) {
+ mResolveRequests.pop();
+ resolveNextRequest();
+ }
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ mListener.onServiceResolved(serviceInfo);
+
+ synchronized (mLock) {
+ mResolveRequests.pop();
+ resolveNextRequest();
+ }
+ }
+ }
+
+ private void resolveNextRequest() {
+ if (!mResolveRequests.isEmpty()) {
+ PendingResolve request = mResolveRequests.getFirst();
+ mNsdManager.resolveService(request.serviceInfo, request.listener);
+ }
+ }
+
+}
diff --git a/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt b/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt
new file mode 100644
index 00000000..d7af9a79
--- /dev/null
+++ b/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+package org.kde.kdeconnect.Helpers
+
+import java.net.Inet4Address
+import java.net.InetAddress
+import java.net.NetworkInterface
+import java.net.SocketException
+
+object NetworkHelper {
+ //Prefer IPv4 over IPv6, because sshfs doesn't seem to like IPv6
+ // Anything with rmnet is related to cellular connections or USB
+ // tethering mechanisms. See:
+ //
+ // https://android.googlesource.com/kernel/msm/+/android-msm-flo-3.4-kitkat-mr1/Documentation/usb/gadget_rmnet.txt
+ //
+ // If we run across an interface that has this, we can safely
+ // ignore it. In fact, it's much safer to do. If we don't, we
+ // might get invalid IP adddresses out of it.
+ @JvmStatic
+ val localIpAddress: InetAddress?
+ get() {
+ var ip6: InetAddress? = null
+ try {
+ for (intf in NetworkInterface.getNetworkInterfaces()) {
+
+ // Anything with rmnet is related to cellular connections or USB
+ // tethering mechanisms. See:
+ //
+ // https://android.googlesource.com/kernel/msm/+/android-msm-flo-3.4-kitkat-mr1/Documentation/usb/gadget_rmnet.txt
+ //
+ // If we run across an interface that has this, we can safely
+ // ignore it. In fact, it's much safer to do. If we don't, we
+ // might get invalid IP adddresses out of it.
+ if (intf.displayName.contains("rmnet")) {
+ continue
+ }
+ val enumIpAddr = intf.inetAddresses
+ while (enumIpAddr.hasMoreElements()) {
+ val inetAddress = enumIpAddr.nextElement()
+ if (!inetAddress.isLoopbackAddress) {
+ ip6 =
+ if (inetAddress is Inet4Address) { //Prefer IPv4 over IPv6, because sshfs doesn't seem to like IPv6
+ return inetAddress
+ } else {
+ inetAddress
+ }
+ }
+ }
+ }
+ } catch (ignored: SocketException) {
+ }
+ return ip6
+ }
+}
diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
index 7d33db92..4d3ac496 100644
--- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
+++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SftpPlugin.java
@@ -20,6 +20,7 @@ import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
+import org.kde.kdeconnect.Helpers.NetworkHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
@@ -147,7 +148,7 @@ public class SftpPlugin extends Plugin implements SharedPreferences.OnSharedPref
NetworkPacket np2 = new NetworkPacket(PACKET_TYPE_SFTP);
- np2.set("ip", server.getLocalIpAddress()); // for backwards compatibility
+ np2.set("ip", NetworkHelper.getLocalIpAddress().getHostAddress());
np2.set("port", server.getPort());
np2.set("user", SimpleSftpServer.USER);
np2.set("password", server.getPassword());
diff --git a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java
index 80d023b1..caa452e5 100644
--- a/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java
+++ b/src/org/kde/kdeconnect/Plugins/SftpPlugin/SimpleSftpServer.java
@@ -26,17 +26,12 @@ import org.kde.kdeconnect.Helpers.RandomHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import java.io.IOException;
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.List;
class SimpleSftpServer {
@@ -145,39 +140,6 @@ class SimpleSftpServer {
return port;
}
- String getLocalIpAddress() {
- String ip6 = null;
- try {
- for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
- NetworkInterface intf = en.nextElement();
-
- // Anything with rmnet is related to cellular connections or USB
- // tethering mechanisms. See:
- //
- // https://android.googlesource.com/kernel/msm/+/android-msm-flo-3.4-kitkat-mr1/Documentation/usb/gadget_rmnet.txt
- //
- // If we run across an interface that has this, we can safely
- // ignore it. In fact, it's much safer to do. If we don't, we
- // might get invalid IP adddresses out of it.
- if (intf.getDisplayName().contains("rmnet")) continue;
-
- for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
- InetAddress inetAddress = enumIpAddr.nextElement();
- if (!inetAddress.isLoopbackAddress()) {
- String address = inetAddress.getHostAddress();
- if (inetAddress instanceof Inet4Address) { //Prefer IPv4 over IPv6, because sshfs doesn't seem to like IPv6
- return address;
- } else {
- ip6 = address;
- }
- }
- }
- }
- } catch (SocketException ignored) {
- }
- return ip6;
- }
-
public boolean isInitialized() {
return initialized;
}
diff --git a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java
index e3e8fd1b..fef0a600 100644
--- a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java
+++ b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java
@@ -37,6 +37,9 @@ import org.kde.kdeconnect_tp.R;
public class SettingsFragment extends PreferenceFragmentCompat {
+ public static final String KEY_UDP_BROADCAST_ENABLED = "udp_broadcast_enabled";
+ public static final String KEY_APP_THEME = "theme_pref";
+
private EditTextPreference renameDevice;
@NonNull
@@ -90,7 +93,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
// Theme Selector
ListPreference themeSelector = new ListPreference(context);
- themeSelector.setKey("theme_pref");
+ themeSelector.setKey(KEY_APP_THEME);
themeSelector.setTitle(R.string.theme_dialog_title);
themeSelector.setDialogTitle(R.string.theme_dialog_title);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
@@ -168,6 +171,12 @@ public class SettingsFragment extends PreferenceFragmentCompat {
return true;
});
+ // UDP broadcast toggle
+ final TwoStatePreference udpBroadcastDiscovery = new SwitchPreference(context);
+ udpBroadcastDiscovery.setDefaultValue(true);
+ udpBroadcastDiscovery.setKey(KEY_UDP_BROADCAST_ENABLED);
+ udpBroadcastDiscovery.setTitle(R.string.enable_udp_broadcast);
+ screen.addPreference(udpBroadcastDiscovery);
// More settings text
Preference moreSettingsText = new Preference(context);
@@ -178,7 +187,5 @@ public class SettingsFragment extends PreferenceFragmentCompat {
screen.addPreference(moreSettingsText);
setPreferenceScreen(screen);
-
-
}
}