From c3e51d13fee3df82a77b25e3227527af16545d4b Mon Sep 17 00:00:00 2001 From: Aleix Pol Date: Wed, 2 Jul 2025 19:29:28 +0200 Subject: [PATCH] virtual-monitor: Make it possible for the device to act as an rdp host --- res/values/strings.xml | 2 + src/org/kde/kdeconnect/Backends/BaseLink.java | 4 + .../Backends/LanBackend/LanLink.java | 5 + src/org/kde/kdeconnect/Device.kt | 11 ++ .../kde/kdeconnect/Helpers/NetworkHelper.kt | 4 +- .../VirtualMonitorPlugin.kt | 114 ++++++++++++++++++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/org/kde/kdeconnect/Plugins/VirtualMonitorPlugin/VirtualMonitorPlugin.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index 8add4548..0890af28 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -393,6 +393,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted Find remote device Ring your remote device Ring + Virtual Monitor + Extend your devices monitors System volume Control the system volume of the remote device diff --git a/src/org/kde/kdeconnect/Backends/BaseLink.java b/src/org/kde/kdeconnect/Backends/BaseLink.java index cc4dacb1..f2aa634c 100644 --- a/src/org/kde/kdeconnect/Backends/BaseLink.java +++ b/src/org/kde/kdeconnect/Backends/BaseLink.java @@ -16,6 +16,8 @@ import org.kde.kdeconnect.DeviceInfo; import org.kde.kdeconnect.NetworkPacket; import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketAddress; import java.util.ArrayList; @@ -43,6 +45,8 @@ public abstract class BaseLink { return getDeviceInfo().id; } + public InetAddress getDeviceIp() { return null; } + public BaseLinkProvider getLinkProvider() { return linkProvider; } diff --git a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java index e3b96ec3..02f21e3c 100644 --- a/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java +++ b/src/org/kde/kdeconnect/Backends/LanBackend/LanLink.java @@ -27,9 +27,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.nio.channels.NotYetConnectedException; @@ -256,4 +258,7 @@ public class LanLink extends BaseLink { packetReceived(np); } + @Override + public InetAddress getDeviceIp() { return socket.getInetAddress(); } + } diff --git a/src/org/kde/kdeconnect/Device.kt b/src/org/kde/kdeconnect/Device.kt index 0e2cc85a..e5e7fbe6 100644 --- a/src/org/kde/kdeconnect/Device.kt +++ b/src/org/kde/kdeconnect/Device.kt @@ -46,6 +46,8 @@ import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap import java.util.concurrent.CopyOnWriteArrayList import androidx.core.content.edit +import java.net.InetAddress +import java.net.SocketAddress class Device : PacketReceiver { @@ -647,6 +649,15 @@ class Device : PacketReceiver { fun removePluginsChangedListener(listener: PluginsChangedListener) = pluginsChangedListeners.remove(listener) + fun ipAddress(): InetAddress? { + for (link in links) { + if (link.deviceIp != null) { + return link.deviceIp + } + } + return null + } + fun disconnect() { links.forEach(BaseLink::disconnect) } diff --git a/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt b/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt index d7af9a79..33bd3e8a 100644 --- a/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt +++ b/src/org/kde/kdeconnect/Helpers/NetworkHelper.kt @@ -19,7 +19,7 @@ object NetworkHelper { // // 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. + // might get invalid IP addresses out of it. @JvmStatic val localIpAddress: InetAddress? get() { @@ -34,7 +34,7 @@ object NetworkHelper { // // 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. + // might get invalid IP addresses out of it. if (intf.displayName.contains("rmnet")) { continue } diff --git a/src/org/kde/kdeconnect/Plugins/VirtualMonitorPlugin/VirtualMonitorPlugin.kt b/src/org/kde/kdeconnect/Plugins/VirtualMonitorPlugin/VirtualMonitorPlugin.kt new file mode 100644 index 00000000..b976498e --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/VirtualMonitorPlugin/VirtualMonitorPlugin.kt @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2025 Aleix Pol i Gonzalez + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +package org.kde.kdeconnect.Plugins.VirtualMonitorPlugin + +import android.content.Intent +import android.graphics.Rect +import android.net.Uri +import android.os.Build +import android.util.Log +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.core.net.toUri +import org.json.JSONArray +import org.json.JSONObject +import org.kde.kdeconnect.NetworkPacket +import org.kde.kdeconnect.Plugins.Plugin +import org.kde.kdeconnect.Plugins.PluginFactory.LoadablePlugin +import org.kde.kdeconnect_tp.R + +const val PACKET_TYPE_VIRTUALMONITOR: String = "kdeconnect.virtualmonitor" +const val PACKET_TYPE_VIRTUALMONITOR_REQUEST: String = "kdeconnect.virtualmonitor.request" + +@LoadablePlugin +class VirtualMonitorPlugin : Plugin() { + + override val displayName: String + get() = context.resources.getString(R.string.pref_plugin_virtualmonitor) + + override val description: String + get() = context.resources.getString(R.string.pref_plugin_virtualmonitor_desc) + + private fun openUrlExternally(url: Uri): Boolean { + val intent = Intent(Intent.ACTION_VIEW, url) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + return true + } else { + return false + } + } + + override fun onPacketReceived(np: NetworkPacket): Boolean { + if (np.type == PACKET_TYPE_VIRTUALMONITOR_REQUEST) { + // At least a password is necessary, we have defaults for all other parameters + if (!np.has("password")) { + Log.e("KDE/VirtualMonitor", "Request invalid, missing password") + return false + } + val addr = device.ipAddress()?.hostAddress + if (addr == null) { + Log.e("KDE/VirtualMonitor", "Request invalid, no address") + return false + } + val protocol = np.getString("protocol") + val username = np.getString("username") + val password = np.getString("password") + val port = np.getInt("port", -1) + + val url = "$protocol://$username:$password@$addr:$port".toUri() + + Log.i("KDE/VirtualMonitor", "Received request, try connecting to $url") + + if (!openUrlExternally(url)) { + Log.e("KDE/VirtualMonitor", "Failed to open $url") + val failure = NetworkPacket(PACKET_TYPE_VIRTUALMONITOR).apply { + this["failed"] = 0 + } + device.sendPacket(failure) + } + } + return true + } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + override fun onCreate() : Boolean + { + val windowManager = ContextCompat.getSystemService(context, WindowManager::class.java) + assert(windowManager != null); + val windowMetrics: WindowMetrics = windowManager!!.currentWindowMetrics + if (device.ipAddress() == null) { + Log.e("KDE/VirtualMonitor", "No IP address for device, pass.") + return false + } + + val bounds: Rect = windowMetrics.bounds + val np = NetworkPacket(PACKET_TYPE_VIRTUALMONITOR).apply { + this["resolutions"] = JSONArray().apply { put(JSONObject().apply { + put("resolution", bounds.width().toString() + 'x' + bounds.height()) + put("scale", windowMetrics.density) + }) } + this["supports_rdp"] = true + this["supports_virt_mon"] = false + } + + device.sendPacket(np) + return true + } + + override fun onUnpairedDevicePacketReceived(np: NetworkPacket): Boolean { + return super.onUnpairedDevicePacketReceived(np) + } + + override val supportedPacketTypes: Array + get() = arrayOf(PACKET_TYPE_VIRTUALMONITOR, PACKET_TYPE_VIRTUALMONITOR_REQUEST) + + override val outgoingPacketTypes: Array + get() = arrayOf(PACKET_TYPE_VIRTUALMONITOR) +}