2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-29 13:17:43 +00:00

virtual-monitor: Make it possible for the device to act as an rdp host

This commit is contained in:
Aleix Pol 2025-07-02 19:29:28 +02:00
parent a733433551
commit c3e51d13fe
6 changed files with 138 additions and 2 deletions

View File

@ -393,6 +393,8 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="pref_plugin_findremotedevice">Find remote device</string> <string name="pref_plugin_findremotedevice">Find remote device</string>
<string name="pref_plugin_findremotedevice_desc">Ring your remote device</string> <string name="pref_plugin_findremotedevice_desc">Ring your remote device</string>
<string name="ring">Ring</string> <string name="ring">Ring</string>
<string name="pref_plugin_virtualmonitor">Virtual Monitor</string>
<string name="pref_plugin_virtualmonitor_desc">Extend your devices monitors</string>
<string name="pref_plugin_systemvolume">System volume</string> <string name="pref_plugin_systemvolume">System volume</string>
<string name="pref_plugin_systemvolume_desc">Control the system volume of the remote device</string> <string name="pref_plugin_systemvolume_desc">Control the system volume of the remote device</string>

View File

@ -16,6 +16,8 @@ import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
@ -43,6 +45,8 @@ public abstract class BaseLink {
return getDeviceInfo().id; return getDeviceInfo().id;
} }
public InetAddress getDeviceIp() { return null; }
public BaseLinkProvider getLinkProvider() { public BaseLinkProvider getLinkProvider() {
return linkProvider; return linkProvider;
} }

View File

@ -27,9 +27,11 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.nio.channels.NotYetConnectedException; import java.nio.channels.NotYetConnectedException;
@ -256,4 +258,7 @@ public class LanLink extends BaseLink {
packetReceived(np); packetReceived(np);
} }
@Override
public InetAddress getDeviceIp() { return socket.getInetAddress(); }
} }

View File

@ -46,6 +46,8 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap import java.util.concurrent.ConcurrentMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import androidx.core.content.edit import androidx.core.content.edit
import java.net.InetAddress
import java.net.SocketAddress
class Device : PacketReceiver { class Device : PacketReceiver {
@ -647,6 +649,15 @@ class Device : PacketReceiver {
fun removePluginsChangedListener(listener: PluginsChangedListener) = pluginsChangedListeners.remove(listener) 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() { fun disconnect() {
links.forEach(BaseLink::disconnect) links.forEach(BaseLink::disconnect)
} }

View File

@ -19,7 +19,7 @@ object NetworkHelper {
// //
// If we run across an interface that has this, we can safely // 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 // 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 @JvmStatic
val localIpAddress: InetAddress? val localIpAddress: InetAddress?
get() { get() {
@ -34,7 +34,7 @@ object NetworkHelper {
// //
// If we run across an interface that has this, we can safely // 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 // 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")) { if (intf.displayName.contains("rmnet")) {
continue continue
} }

View File

@ -0,0 +1,114 @@
/*
* SPDX-FileCopyrightText: 2025 Aleix Pol i Gonzalez <aleixpol@kde.org>
*
* 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<String>
get() = arrayOf(PACKET_TYPE_VIRTUALMONITOR, PACKET_TYPE_VIRTUALMONITOR_REQUEST)
override val outgoingPacketTypes: Array<String>
get() = arrayOf(PACKET_TYPE_VIRTUALMONITOR)
}