2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 01:51:47 +00:00

Merge branch 'work/deviceinfo' into work/all

This commit is contained in:
Albert Vaca Cintora 2023-06-20 10:53:49 +02:00
commit 268bc833be
18 changed files with 364 additions and 286 deletions

View File

@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import java.io.IOException; import java.io.IOException;
@ -26,20 +27,20 @@ public abstract class BaseLink {
protected final Context context; protected final Context context;
private final BaseLinkProvider linkProvider; private final BaseLinkProvider linkProvider;
private final String deviceId;
private final ArrayList<PacketReceiver> receivers = new ArrayList<>(); private final ArrayList<PacketReceiver> receivers = new ArrayList<>();
protected BaseLink(@NonNull Context context, @NonNull String deviceId, @NonNull BaseLinkProvider linkProvider) { protected BaseLink(@NonNull Context context, @NonNull BaseLinkProvider linkProvider) {
this.context = context; this.context = context;
this.linkProvider = linkProvider; this.linkProvider = linkProvider;
this.deviceId = deviceId;
} }
/* To be implemented by each link for pairing handlers */ /* To be implemented by each link for pairing handlers */
public abstract String getName(); public abstract String getName();
public abstract DeviceInfo getDeviceInfo();
public String getDeviceId() { public String getDeviceId() {
return deviceId; return getDeviceInfo().id;
} }
public BaseLinkProvider getLinkProvider() { public BaseLinkProvider getLinkProvider() {
@ -54,7 +55,7 @@ public abstract class BaseLink {
} }
//Should be called from a background thread listening for packets //Should be called from a background thread listening for packets
protected void packetReceived(@NonNull NetworkPacket np) { public void packetReceived(@NonNull NetworkPacket np) {
for(PacketReceiver pr : receivers) { for(PacketReceiver pr : receivers) {
pr.onPacketReceived(np); pr.onPacketReceived(np);
} }

View File

@ -8,23 +8,17 @@ package org.kde.kdeconnect.Backends;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.kde.kdeconnect.NetworkPacket;
import java.security.cert.Certificate;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
public abstract class BaseLinkProvider { public abstract class BaseLinkProvider {
private final CopyOnWriteArrayList<ConnectionReceiver> connectionReceivers = new CopyOnWriteArrayList<>();
public interface ConnectionReceiver { public interface ConnectionReceiver {
void onConnectionReceived(@NonNull final String deviceId, void onConnectionReceived(@NonNull final BaseLink link);
@NonNull final Certificate certificate,
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link);
void onConnectionLost(BaseLink link); void onConnectionLost(BaseLink link);
} }
private final CopyOnWriteArrayList<ConnectionReceiver> connectionReceivers = new CopyOnWriteArrayList<>();
public void addConnectionReceiver(ConnectionReceiver cr) { public void addConnectionReceiver(ConnectionReceiver cr) {
connectionReceivers.add(cr); connectionReceivers.add(cr);
} }
@ -36,13 +30,10 @@ public abstract class BaseLinkProvider {
/** /**
* To be called from the child classes when a link to a new device is established * To be called from the child classes when a link to a new device is established
*/ */
protected void onConnectionReceived(@NonNull final String deviceId, protected void onConnectionReceived(@NonNull final BaseLink link) {
@NonNull final Certificate certificate,
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link) {
//Log.i("KDE/LinkProvider", "onConnectionReceived"); //Log.i("KDE/LinkProvider", "onConnectionReceived");
for(ConnectionReceiver cr : connectionReceivers) { for(ConnectionReceiver cr : connectionReceivers) {
cr.onConnectionReceived(deviceId, certificate, identityPacket, link); cr.onConnectionReceived(link);
} }
} }

View File

@ -17,6 +17,7 @@ import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import java.io.IOException; import java.io.IOException;
@ -34,6 +35,7 @@ public class BluetoothLink extends BaseLink {
private final OutputStream output; private final OutputStream output;
private final BluetoothDevice remoteAddress; private final BluetoothDevice remoteAddress;
private final BluetoothLinkProvider linkProvider; private final BluetoothLinkProvider linkProvider;
private final DeviceInfo deviceInfo;
private boolean continueAccepting = true; private boolean continueAccepting = true;
@ -93,11 +95,12 @@ public class BluetoothLink extends BaseLink {
} }
}); });
public BluetoothLink(Context context, ConnectionMultiplexer connection, InputStream input, OutputStream output, BluetoothDevice remoteAddress, String deviceId, BluetoothLinkProvider linkProvider) { public BluetoothLink(Context context, ConnectionMultiplexer connection, InputStream input, OutputStream output, BluetoothDevice remoteAddress, DeviceInfo deviceInfo, BluetoothLinkProvider linkProvider) {
super(context, deviceId, linkProvider); super(context, linkProvider);
this.connection = connection; this.connection = connection;
this.input = input; this.input = input;
this.output = output; this.output = output;
this.deviceInfo = deviceInfo;
this.remoteAddress = remoteAddress; this.remoteAddress = remoteAddress;
this.linkProvider = linkProvider; this.linkProvider = linkProvider;
} }
@ -111,6 +114,11 @@ public class BluetoothLink extends BaseLink {
return "BluetoothLink"; return "BluetoothLink";
} }
@Override
public DeviceInfo getDeviceInfo() {
return deviceInfo;
}
public void disconnect() { public void disconnect() {
if (connection == null) { if (connection == null) {
return; return;
@ -120,7 +128,7 @@ public class BluetoothLink extends BaseLink {
connection.close(); connection.close();
} catch (IOException ignored) { } catch (IOException ignored) {
} }
linkProvider.disconnectedLink(this, getDeviceId(), remoteAddress); linkProvider.disconnectedLink(this, remoteAddress);
} }
private void sendMessage(NetworkPacket np) throws JSONException, IOException { private void sendMessage(NetworkPacket np) throws JSONException, IOException {

View File

@ -20,6 +20,8 @@ import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper; import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
@ -54,10 +56,6 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException { private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException {
String deviceId = identityPacket.getString("deviceId"); String deviceId = identityPacket.getString("deviceId");
String certificateString = identityPacket.getString("certificate");
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
Log.i("BluetoothLinkProvider", "addLink to " + deviceId); Log.i("BluetoothLinkProvider", "addLink to " + deviceId);
BluetoothLink oldLink = visibleDevices.get(deviceId); BluetoothLink oldLink = visibleDevices.get(deviceId);
if (oldLink == link) { if (oldLink == link) {
@ -65,8 +63,9 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
return; return;
} }
visibleDevices.put(deviceId, link); visibleDevices.put(deviceId, link);
onConnectionReceived(deviceId, certificate, identityPacket, link); onConnectionReceived(link);
link.startListening(); link.startListening();
link.packetReceived(identityPacket);
if (oldLink != null) { if (oldLink != null) {
Log.i("BluetoothLinkProvider", "Removing old connection to same device"); Log.i("BluetoothLinkProvider", "Removing old connection to same device");
oldLink.disconnect(); oldLink.disconnect();
@ -127,9 +126,9 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
return "BluetoothLinkProvider"; return "BluetoothLinkProvider";
} }
public void disconnectedLink(BluetoothLink link, String deviceId, BluetoothDevice remoteAddress) { public void disconnectedLink(BluetoothLink link, BluetoothDevice remoteAddress) {
sockets.remove(remoteAddress); sockets.remove(remoteAddress);
visibleDevices.remove(deviceId); visibleDevices.remove(link.getDeviceId());
onConnectionLost(link); onConnectionLost(link);
} }
@ -196,8 +195,10 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
OutputStream outputStream = connection.getDefaultOutputStream(); OutputStream outputStream = connection.getDefaultOutputStream();
InputStream inputStream = connection.getDefaultInputStream(); InputStream inputStream = connection.getDefaultInputStream();
NetworkPacket np = NetworkPacket.createIdentityPacket(context); DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket np = myDeviceInfo.toIdentityPacket();
np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0)); np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
byte[] message = np.serialize().getBytes(Charsets.UTF_8); byte[] message = np.serialize().getBytes(Charsets.UTF_8);
outputStream.write(message); outputStream.write(message);
outputStream.flush(); outputStream.flush();
@ -223,9 +224,15 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.i("BTLinkProvider/Server", "Received identity packet"); Log.i("BTLinkProvider/Server", "Received identity packet");
String certificateString = identityPacket.getString("certificate");
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
BluetoothLink link = new BluetoothLink(context, connection, BluetoothLink link = new BluetoothLink(context, connection,
inputStream, outputStream, socket.getRemoteDevice(), inputStream, outputStream, socket.getRemoteDevice(),
identityPacket.getString("deviceId"), BluetoothLinkProvider.this); deviceInfo, BluetoothLinkProvider.this);
addLink(identityPacket, link); addLink(identityPacket, link);
} catch (Exception e) { } catch (Exception e) {
synchronized (sockets) { synchronized (sockets) {
@ -360,7 +367,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.i("BTLinkProvider/Client", "Received identity packet"); Log.i("BTLinkProvider/Client", "Received identity packet");
String myId = NetworkPacket.createIdentityPacket(context).getString("deviceId"); String myId = DeviceHelper.getDeviceId(context);
if (identityPacket.getString("deviceId").equals(myId)) { if (identityPacket.getString("deviceId").equals(myId)) {
// Probably won't happen, but just to be safe // Probably won't happen, but just to be safe
connection.close(); connection.close();
@ -373,10 +380,18 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.i("BTLinkProvider/Client", "identity packet received, creating link"); Log.i("BTLinkProvider/Client", "identity packet received, creating link");
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream, String certificateString = identityPacket.getString("certificate");
socket.getRemoteDevice(), identityPacket.getString("deviceId"), BluetoothLinkProvider.this); byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream,
socket.getRemoteDevice(), deviceInfo, BluetoothLinkProvider.this);
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket np2 = myDeviceInfo.toIdentityPacket();
np2.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
NetworkPacket np2 = NetworkPacket.createIdentityPacket(context);
link.sendPacket(np2, new Device.SendPacketStatusCallback() { link.sendPacket(np2, new Device.SendPacketStatusCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {

View File

@ -16,6 +16,7 @@ import org.json.JSONObject;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper; import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
@ -42,6 +43,8 @@ public class LanLink extends BaseLink {
Locally, Remotely Locally, Remotely
} }
private final DeviceInfo deviceInfo;
private volatile SSLSocket socket = null; private volatile SSLSocket socket = null;
@Override @Override
@ -99,8 +102,9 @@ public class LanLink extends BaseLink {
return oldSocket; return oldSocket;
} }
public LanLink(Context context, String deviceId, BaseLinkProvider linkProvider, SSLSocket socket) throws IOException { public LanLink(@NonNull Context context, @NonNull DeviceInfo deviceInfo, @NonNull BaseLinkProvider linkProvider, @NonNull SSLSocket socket) throws IOException {
super(context, deviceId, linkProvider); super(context, linkProvider);
this.deviceInfo = deviceInfo;
reset(socket); reset(socket);
} }
@ -109,6 +113,11 @@ public class LanLink extends BaseLink {
return "LanLink"; return "LanLink";
} }
@Override
public DeviceInfo getDeviceInfo() {
return deviceInfo;
}
@WorkerThread @WorkerThread
@Override @Override
public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) { public boolean sendPacket(@NonNull NetworkPacket np, @NonNull final Device.SendPacketStatusCallback callback, boolean sendPayloadFromSameThread) {

View File

@ -17,6 +17,7 @@ import org.json.JSONException;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper; import org.kde.kdeconnect.Helpers.ThreadHelper;
@ -51,7 +52,7 @@ import kotlin.text.Charsets;
/** /**
* This LanLinkProvider creates {@link LanLink}s to other devices on the same * This LanLinkProvider creates {@link LanLink}s to other devices on the same
* WiFi network. The first packet sent over a socket must be an * WiFi network. The first packet sent over a socket must be an
* {@link NetworkPacket#createIdentityPacket(Context)}. * {@link DeviceInfo#toIdentityPacket()}.
* *
* @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted) * @see #identityPacketReceived(NetworkPacket, Socket, LanLink.ConnectionStarted)
*/ */
@ -136,8 +137,10 @@ public class LanLinkProvider extends BaseLinkProvider {
Socket socket = socketFactory.createSocket(address, tcpPort); Socket socket = socketFactory.createSocket(address, tcpPort);
configureSocket(socket); configureSocket(socket);
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket myIdentity = myDeviceInfo.toIdentityPacket();
OutputStream out = socket.getOutputStream(); OutputStream out = socket.getOutputStream();
NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context);
out.write(myIdentity.serialize().getBytes()); out.write(myIdentity.serialize().getBytes());
out.flush(); out.flush();
@ -193,17 +196,19 @@ public class LanLinkProvider extends BaseLinkProvider {
identityPacketReceived(identityPacket, socket, connectionStarted); identityPacketReceived(identityPacket, socket, connectionStarted);
} }
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted); String deviceName = identityPacket.getString("deviceName", "unknown");
Log.i("KDE/LanLinkProvider", "Starting SSL handshake with " + deviceName + " trusted:" + isDeviceTrusted);
final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode); final SSLSocket sslSocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
sslSocket.addHandshakeCompletedListener(event -> { sslSocket.addHandshakeCompletedListener(event -> {
String mode = clientMode ? "client" : "server"; String mode = clientMode ? "client" : "server";
try { try {
Certificate certificate = event.getPeerCertificates()[0]; Certificate certificate = event.getPeerCertificates()[0];
Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite()); DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
addLink(deviceId, certificate, identityPacket, sslSocket); Log.i("KDE/LanLinkProvider", "Handshake as " + mode + " successful with " + deviceName + " secured with " + event.getCipherSuite());
} catch (Exception e) { addLink(sslSocket, deviceInfo);
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e); } catch (IOException e) {
Log.e("KDE/LanLinkProvider", "Handshake as " + mode + " failed with " + deviceName, e);
Device device = KdeConnect.getInstance().getDevice(deviceId); Device device = KdeConnect.getInstance().getDevice(deviceId);
if (device == null) { if (device == null) {
return; return;
@ -221,25 +226,24 @@ public class LanLinkProvider extends BaseLinkProvider {
/** /**
* Add or update a link in the {@link #visibleDevices} map. * Add or update a link in the {@link #visibleDevices} map.
* *
* @param deviceId remote device id
* @param certificate remote device certificate
* @param identityPacket identity packet with the remote device's device name, type, protocol version, etc.
* @param socket a new Socket, which should be used to send and receive packets from the remote device * @param socket a new Socket, which should be used to send and receive packets from the remote device
* @param deviceInfo remote device info
* @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket)} * @throws IOException if an exception is thrown by {@link LanLink#reset(SSLSocket)}
*/ */
private void addLink(String deviceId, Certificate certificate, final NetworkPacket identityPacket, SSLSocket socket) throws IOException { private LanLink addLink(SSLSocket socket, DeviceInfo deviceInfo) throws IOException {
LanLink currentLink = visibleDevices.get(deviceId); LanLink link = visibleDevices.get(deviceInfo.id);
if (currentLink != null) { if (link != null) {
//Update old link // Update existing link
Log.i("KDE/LanLinkProvider", "Reusing same link for device " + deviceId); Log.d("KDE/LanLinkProvider", "Reusing same link for device " + deviceInfo.id);
final Socket oldSocket = currentLink.reset(socket); final Socket oldSocket = link.reset(socket);
} else { } else {
Log.i("KDE/LanLinkProvider", "Creating a new link for device " + deviceId); // Create a new link
//Let's create the link Log.d("KDE/LanLinkProvider", "Creating a new link for device " + deviceInfo.id);
LanLink link = new LanLink(context, deviceId, this, socket); link = new LanLink(context, deviceInfo, this, socket);
visibleDevices.put(deviceId, link); visibleDevices.put(deviceInfo.id, link);
onConnectionReceived(deviceId, certificate, identityPacket, link); onConnectionReceived(link);
} }
return link;
} }
public LanLinkProvider(Context context) { public LanLinkProvider(Context context) {
@ -378,7 +382,8 @@ public class LanLinkProvider extends BaseLinkProvider {
return; return;
} }
NetworkPacket identity = NetworkPacket.createIdentityPacket(context); DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket identity = myDeviceInfo.toIdentityPacket();
identity.set("tcpPort", tcpServer.getLocalPort()); identity.set("tcpPort", tcpServer.getLocalPort());
byte[] bytes; byte[] bytes;

View File

@ -14,12 +14,14 @@ import androidx.annotation.WorkerThread;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
public class LoopbackLink extends BaseLink { public class LoopbackLink extends BaseLink {
public LoopbackLink(Context context, BaseLinkProvider linkProvider) { public LoopbackLink(Context context, BaseLinkProvider linkProvider) {
super(context, "loopback", linkProvider); super(context, linkProvider);
} }
@Override @Override
@ -40,4 +42,9 @@ public class LoopbackLink extends BaseLink {
return true; return true;
} }
@Override
public DeviceInfo getDeviceInfo() {
return DeviceHelper.getDeviceInfo(context);
}
} }

View File

@ -9,9 +9,6 @@ package org.kde.kdeconnect.Backends.LoopbackBackend;
import android.content.Context; import android.content.Context;
import org.kde.kdeconnect.Backends.BaseLinkProvider; import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.NetworkPacket;
public class LoopbackLinkProvider extends BaseLinkProvider { public class LoopbackLinkProvider extends BaseLinkProvider {
@ -32,9 +29,8 @@ public class LoopbackLinkProvider extends BaseLinkProvider {
@Override @Override
public void onNetworkChange() { public void onNetworkChange() {
NetworkPacket np = NetworkPacket.createIdentityPacket(context); LoopbackLink link = new LoopbackLink(context, this);
String deviceId = DeviceHelper.getDeviceId(context); onConnectionReceived(link);
onConnectionReceived(deviceId, SslHelper.certificate, np, new LoopbackLink(context, this));
} }
@Override @Override

View File

@ -14,7 +14,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import androidx.annotation.AnyThread; import androidx.annotation.AnyThread;
@ -26,7 +25,6 @@ import androidx.core.content.ContextCompat;
import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
import org.apache.commons.lang3.StringUtils;
import org.kde.kdeconnect.Backends.BaseLink; import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.Helpers.NotificationHelper;
@ -38,13 +36,8 @@ import org.kde.kdeconnect_tp.R;
import java.io.IOException; import java.io.IOException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.Vector; import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -53,30 +46,26 @@ public class Device implements BaseLink.PacketReceiver {
private final Context context; private final Context context;
private final String deviceId; final DeviceInfo deviceInfo;
private String name;
public Certificate certificate;
private int notificationId; private int notificationId;
private int protocolVersion;
private DeviceType deviceType;
PairingHandler pairingHandler; PairingHandler pairingHandler;
private final CopyOnWriteArrayList<PairingHandler.PairingCallback> pairingCallbacks = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<PairingHandler.PairingCallback> pairingCallbacks = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<BaseLink> links = new CopyOnWriteArrayList<>();
private DevicePacketQueue packetQueue; private DevicePacketQueue packetQueue;
private List<String> supportedPlugins = new ArrayList<>(); private List<String> supportedPlugins;
private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Plugin> plugins = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Plugin> pluginsWithoutPermissions = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Plugin> pluginsWithoutOptionalPermissions = new ConcurrentHashMap<>();
private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>(); private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>();
private final SharedPreferences settings; private final SharedPreferences settings;
private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();
private Set<String> incomingCapabilities = new HashSet<>();
public boolean supportsPacketType(String type) { public boolean supportsPacketType(String type) {
if (incomingCapabilities == null) { if (deviceInfo.incomingCapabilities == null) {
return true; return true;
} else { } else {
return incomingCapabilities.contains(type); return deviceInfo.incomingCapabilities.contains(type);
} }
} }
@ -84,109 +73,52 @@ public class Device implements BaseLink.PacketReceiver {
void onPluginsChanged(@NonNull Device device); void onPluginsChanged(@NonNull Device device);
} }
public enum DeviceType { /**
Phone, * Constructor for remembered, already-trusted devices.
Tablet, * Given the deviceId, it will load the other properties from SharedPreferences.
Computer, */
Tv; Device(@NonNull Context context, @NonNull String deviceId) {
static public DeviceType FromString(String s) {
if ("tablet".equals(s)) return Tablet;
if ("phone".equals(s)) return Phone;
if ("tv".equals(s)) return Tv;
return Computer; //Default
}
@NonNull
public String toString() {
switch (this) {
case Tablet:
return "tablet";
case Phone:
return "phone";
case Tv:
return "tv";
default:
return "desktop";
}
}
public Drawable getIcon(Context context) {
int drawableId;
switch (this) {
case Phone:
drawableId = R.drawable.ic_device_phone_32dp;
break;
case Tablet:
drawableId = R.drawable.ic_device_tablet_32dp;
break;
case Tv:
drawableId = R.drawable.ic_device_tv_32dp;
break;
default:
drawableId = R.drawable.ic_device_laptop_32dp;
}
return ContextCompat.getDrawable(context, drawableId);
}
}
// Remembered trusted device, we need to wait for a incoming Link to communicate
Device(@NonNull Context context, @NonNull String deviceId) throws CertificateException {
this.context = context; this.context = context;
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE);
this.deviceInfo = DeviceInfo.loadFromSettings(context, deviceId, settings);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired); this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired);
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
this.deviceId = deviceId; Log.i("Device","Loading trusted device: " + deviceInfo.name);
this.name = settings.getString("deviceName", context.getString(R.string.unknown_device));
this.protocolVersion = 0; //We don't know it yet
this.deviceType = DeviceType.FromString(settings.getString("deviceType", "desktop"));
this.certificate = SslHelper.getDeviceCertificate(context, deviceId);
Log.i("Device","Loading trusted device: " + this.name);
//Assume every plugin is supported until addLink is called and we can get the actual list
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
//Do not load plugins yet, the device is not present
//reloadPluginsFromSettings();
} }
// Device known via an incoming connection sent to us via a Link, we don't trust it yet /**
Device(@NonNull Context context, @NonNull String deviceId, @NonNull Certificate certificate, @NonNull NetworkPacket identityPacket, @NonNull BaseLink dl) { * Constructor for devices discovered but not trusted yet.
Log.i("Device","Creating untrusted device"); * Gets the DeviceInfo by calling link.getDeviceInfo() on the link passed.
* This constructor also calls addLink() with the link you pass to it, since it's not legal to have an unpaired Device with 0 links.
*/
Device(@NonNull Context context, @NonNull BaseLink link) {
this.context = context; this.context = context;
this.deviceInfo = link.getDeviceInfo();
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired); this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired);
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
this.deviceId = deviceId; Log.i("Device","Creating untrusted device: "+ deviceInfo.name);
this.certificate = certificate; addLink(link);
// The following properties are read from the identityPacket in addLink since they can change in future identity packets
this.name = context.getString(R.string.unknown_device);
this.deviceType = DeviceType.Computer;
this.protocolVersion = 0;
addLink(identityPacket, dl);
} }
public String getName() { public String getName() {
return StringUtils.defaultString(name, context.getString(R.string.unknown_device)); return deviceInfo.name;
} }
public Drawable getIcon() { public Drawable getIcon() {
return deviceType.getIcon(context); return deviceInfo.type.getIcon(context);
} }
public DeviceType getDeviceType() { public DeviceType getDeviceType() {
return deviceType; return deviceInfo.type;
} }
public String getDeviceId() { public String getDeviceId() {
return deviceId; return deviceInfo.id;
}
public Certificate getCertificate() {
return deviceInfo.certificate;
} }
public Context getContext() { public Context getContext() {
@ -195,7 +127,7 @@ public class Device implements BaseLink.PacketReceiver {
//Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer //Returns 0 if the version matches, < 0 if it is older or > 0 if it is newer
public int compareProtocolVersion() { public int compareProtocolVersion() {
return protocolVersion - DeviceHelper.ProtocolVersion; return deviceInfo.protocolVersion - DeviceHelper.ProtocolVersion;
} }
@ -257,20 +189,11 @@ public class Device implements BaseLink.PacketReceiver {
hidePairingNotification(); hidePairingNotification();
// Store current device certificate so we can check it in the future (TOFU) // Store current device certificate so we can check it in the future (TOFU)
SharedPreferences.Editor editor = context.getSharedPreferences(getDeviceId(), Context.MODE_PRIVATE).edit(); deviceInfo.saveInSettings(Device.this.settings);
try {
String encodedCertificate = Base64.encodeToString(certificate.getEncoded(), 0);
editor.putString("certificate", encodedCertificate);
} catch(CertificateEncodingException e) {
throw new RuntimeException(e);
}
editor.putString("deviceName", name);
editor.putString("deviceType", deviceType.toString());
editor.apply();
// Store as trusted device // Store as trusted device
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().putBoolean(deviceId, true).apply(); preferences.edit().putBoolean(deviceInfo.id, true).apply();
reloadPluginsFromSettings(); reloadPluginsFromSettings();
@ -290,9 +213,9 @@ public class Device implements BaseLink.PacketReceiver {
@Override @Override
public void unpaired() { public void unpaired() {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE); SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
preferences.edit().remove(deviceId).apply(); preferences.edit().remove(deviceInfo.id).apply();
SharedPreferences devicePreferences = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE); SharedPreferences devicePreferences = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
devicePreferences.edit().clear().apply(); devicePreferences.edit().clear().apply();
for (PairingHandler.PairingCallback cb : pairingCallbacks) { for (PairingHandler.PairingCallback cb : pairingCallbacks) {
@ -334,7 +257,7 @@ public class Device implements BaseLink.PacketReceiver {
final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class); final NotificationManager notificationManager = ContextCompat.getSystemService(getContext(), NotificationManager.class);
String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, certificate).substring(8); String verificationKeyShort = SslHelper.getVerificationKey(SslHelper.certificate, deviceInfo.certificate).substring(8);
Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT) Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT)
.setContentTitle(res.getString(R.string.pairing_request_from, getName())) .setContentTitle(res.getString(R.string.pairing_request_from, getName()))
@ -365,7 +288,7 @@ public class Device implements BaseLink.PacketReceiver {
return !links.isEmpty(); return !links.isEmpty();
} }
public void addLink(NetworkPacket identityPacket, BaseLink link) { public void addLink(BaseLink link) {
if (links.isEmpty()) { if (links.isEmpty()) {
packetQueue = new DevicePacketQueue(this); packetQueue = new DevicePacketQueue(this);
} }
@ -373,33 +296,11 @@ public class Device implements BaseLink.PacketReceiver {
links.add(link); links.add(link);
link.addPacketReceiver(this); link.addPacketReceiver(this);
this.protocolVersion = identityPacket.getInt("protocolVersion"); boolean hasChanges = updateDeviceInfo(link.getDeviceInfo());
if (identityPacket.has("deviceName")) { if (hasChanges || links.size() == 1) {
this.name = identityPacket.getString("deviceName", this.name); reloadPluginsFromSettings();
SharedPreferences.Editor editor = settings.edit();
editor.putString("deviceName", this.name);
editor.apply();
} }
if (identityPacket.has("deviceType")) {
this.deviceType = DeviceType.FromString(identityPacket.getString("deviceType", "desktop"));
}
Log.i("KDE/Device", "addLink " + link.getLinkProvider().getName() + " -> " + getName() + " active links: " + links.size());
Set<String> outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities", null);
Set<String> incomingCapabilities = identityPacket.getStringSet("incomingCapabilities", null);
if (incomingCapabilities != null && outgoingCapabilities != null) {
supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(incomingCapabilities, outgoingCapabilities));
} else {
supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins());
}
this.incomingCapabilities = incomingCapabilities;
reloadPluginsFromSettings();
} }
public void removeLink(BaseLink link) { public void removeLink(BaseLink link) {
@ -417,6 +318,30 @@ public class Device implements BaseLink.PacketReceiver {
} }
} }
public boolean updateDeviceInfo(@NonNull DeviceInfo newDeviceInfo) {
boolean hasChanges = false;
if (!deviceInfo.name.equals(newDeviceInfo.name) || deviceInfo.type != newDeviceInfo.type) {
hasChanges = true;
deviceInfo.name = newDeviceInfo.name;
deviceInfo.type = newDeviceInfo.type;
if (isPaired()) {
deviceInfo.saveInSettings(settings);
}
}
if (deviceInfo.outgoingCapabilities != newDeviceInfo.outgoingCapabilities ||
deviceInfo.incomingCapabilities != newDeviceInfo.incomingCapabilities) {
if (newDeviceInfo.outgoingCapabilities != null && newDeviceInfo.incomingCapabilities != null) {
hasChanges = true;
Log.i("updateDeviceInfo", "Updating supported plugins according to new capabilities");
supportedPlugins = new Vector<>(PluginFactory.pluginsForCapabilities(newDeviceInfo.incomingCapabilities, newDeviceInfo.outgoingCapabilities));
}
}
return hasChanges;
}
@Override @Override
public void onPacketReceived(@NonNull NetworkPacket np) { public void onPacketReceived(@NonNull NetworkPacket np) {
@ -580,7 +505,7 @@ public class Device implements BaseLink.PacketReceiver {
} }
if (!success) { if (!success) {
Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + name + " lost!"); Log.e("KDE/sendPacket", "No device link (of " + links.size() + " available) could send the packet. Packet " + np.getType() + " to " + deviceInfo.name + " lost!");
} }
return success; return success;
@ -697,6 +622,7 @@ public class Device implements BaseLink.PacketReceiver {
} }
public void reloadPluginsFromSettings() { public void reloadPluginsFromSettings() {
Log.i("Device", deviceInfo.name +": reloading plugins");
MultiValuedMap<String, String> newPluginsByIncomingInterface = new ArrayListValuedHashMap<>(); MultiValuedMap<String, String> newPluginsByIncomingInterface = new ArrayListValuedHashMap<>();
for (String pluginKey : supportedPlugins) { for (String pluginKey : supportedPlugins) {

View File

@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect
import android.content.Context
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.content.ContextCompat
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper
import org.kde.kdeconnect_tp.R
import java.security.cert.Certificate
import java.security.cert.CertificateEncodingException
/**
* DeviceInfo contains all the properties needed to instantiate a Device.
*/
class DeviceInfo(
@JvmField val id : String,
@JvmField val certificate : Certificate,
@JvmField var name : String,
@JvmField var type : DeviceType,
@JvmField var protocolVersion : Int = 0,
@JvmField var incomingCapabilities : Set<String>? = null,
@JvmField var outgoingCapabilities : Set<String>? = null,
) {
/**
* Saves the info in settings so it can be restored later using loadFromSettings().
* This is used to keep info from paired devices, even when they are not reachable.
* The capabilities and protocol version are not persisted.
*/
fun saveInSettings(settings: SharedPreferences) {
val editor = settings.edit()
try {
val encodedCertificate = Base64.encodeToString(certificate.encoded, 0)
editor.putString("certificate", encodedCertificate)
} catch (e: CertificateEncodingException) {
throw RuntimeException(e)
}
editor.putString("deviceName", name)
editor.putString("deviceType", type.toString())
editor.apply()
}
/**
* Serializes to a NetworkPacket, which LanLinkProvider uses to send this data over the network.
* The serialization doesn't include the certificate, since LanLink can query that from the socket.
* Can be deserialized using fromIdentityPacketAndCert(), given a certificate.
*/
fun toIdentityPacket(): NetworkPacket {
val np = NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY)
np.set("deviceId", id)
np.set("deviceName", name)
np.set("protocolVersion", protocolVersion)
np.set("deviceType", type.toString())
np.set("incomingCapabilities", incomingCapabilities)
np.set("outgoingCapabilities", outgoingCapabilities)
return np
}
companion object {
/**
* Recreates a DeviceInfo object that was persisted using saveInSettings()
*/
@JvmStatic
fun loadFromSettings(context : Context, deviceId: String, settings: SharedPreferences): DeviceInfo {
val deviceName = settings.getString("deviceName", "unknown")!!
val deviceType = DeviceType.fromString(settings.getString("deviceType", "desktop")!!)
val certificate = SslHelper.getDeviceCertificate(context, deviceId)
return DeviceInfo(id = deviceId, name = deviceName, type = deviceType, certificate = certificate)
}
/**
* Recreates a DeviceInfo object that was serialized using toIdentityPacket().
* Since toIdentityPacket() doesn't serialize the certificate, this needs to be passed separately.
*/
@JvmStatic
fun fromIdentityPacketAndCert(identityPacket : NetworkPacket, certificate : Certificate): DeviceInfo {
val deviceId = identityPacket.getString("deviceId")
val deviceName = identityPacket.getString("deviceName", "unknown")
val protocolVersion = identityPacket.getInt("protocolVersion")
val deviceType = DeviceType.fromString(identityPacket.getString("deviceType", "desktop"))
val incomingCapabilities = identityPacket.getStringSet("incomingCapabilities")
val outgoingCapabilities = identityPacket.getStringSet("outgoingCapabilities")
return DeviceInfo(id = deviceId, name = deviceName, type = deviceType, certificate = certificate,
protocolVersion = protocolVersion, incomingCapabilities = incomingCapabilities, outgoingCapabilities = outgoingCapabilities)
}
}
}
enum class DeviceType {
Phone, Tablet, Computer, Tv;
override fun toString(): String {
return when (this) {
Tablet -> "tablet"
Phone -> "phone"
Tv -> "tv"
else -> "desktop"
}
}
fun getIcon(context: Context): Drawable? {
val drawableId: Int = when (this) {
Phone -> R.drawable.ic_device_phone_32dp
Tablet -> R.drawable.ic_device_tablet_32dp
Tv -> R.drawable.ic_device_tv_32dp
else -> R.drawable.ic_device_laptop_32dp
}
return ContextCompat.getDrawable(context, drawableId)
}
companion object {
@JvmStatic
fun fromString(s: String): DeviceType {
return when (s) {
"phone" -> Phone
"tablet" -> Tablet
"tv" -> Tv
else -> Computer
}
}
}
}

View File

@ -19,7 +19,10 @@ import android.util.Log;
import com.univocity.parsers.csv.CsvParser; import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings; import com.univocity.parsers.csv.CsvParserSettings;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.DeviceInfo;
import org.kde.kdeconnect.DeviceType;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.PluginFactory;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@ -53,13 +56,13 @@ public class DeviceHelper {
return (uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION; return (uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION;
} }
public static Device.DeviceType getDeviceType(Context context) { public static DeviceType getDeviceType(Context context) {
if (isTv(context)) { if (isTv(context)) {
return Device.DeviceType.Tv; return DeviceType.Tv;
} else if (isTablet()) { } else if (isTablet()) {
return Device.DeviceType.Tablet; return DeviceType.Tablet;
} else { } else {
return Device.DeviceType.Phone; return DeviceType.Phone;
} }
} }
@ -146,4 +149,14 @@ public class DeviceHelper {
return preferences.getString(KEY_DEVICE_ID_PREFERENCE, null); return preferences.getString(KEY_DEVICE_ID_PREFERENCE, null);
} }
public static DeviceInfo getDeviceInfo(Context context) {
return new DeviceInfo(getDeviceId(context),
SslHelper.certificate,
getDeviceName(context),
DeviceHelper.getDeviceType(context),
ProtocolVersion,
PluginFactory.getIncomingCapabilities(),
PluginFactory.getOutgoingCapabilities());
}
} }

View File

@ -2,7 +2,7 @@
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com> * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
* *
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/ */
package org.kde.kdeconnect; package org.kde.kdeconnect;
@ -24,8 +24,6 @@ import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect.UserInterface.ThemeUtil;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -115,14 +113,9 @@ public class KdeConnect extends Application {
for (String deviceId : trustedDevices) { for (String deviceId : trustedDevices) {
//Log.e("BackgroundService", "Loading device "+deviceId); //Log.e("BackgroundService", "Loading device "+deviceId);
if (preferences.getBoolean(deviceId, false)) { if (preferences.getBoolean(deviceId, false)) {
try { Device device = new Device(this, deviceId);
Device device = new Device(this, deviceId); devices.put(deviceId, device);
devices.put(deviceId, device); device.addPairingCallback(devicePairingCallback);
device.addPairingCallback(devicePairingCallback);
} catch (CertificateException e) {
Log.e("KdeConnect", "Could not load trusted device, certificate not valid: " + deviceId);
e.printStackTrace();
}
} }
} }
} }
@ -151,18 +144,13 @@ public class KdeConnect extends Application {
private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() { private final BaseLinkProvider.ConnectionReceiver connectionListener = new BaseLinkProvider.ConnectionReceiver() {
@Override @Override
public void onConnectionReceived(@NonNull final String deviceId, public void onConnectionReceived(@NonNull final BaseLink link) {
@NonNull final Certificate certificate, Device device = devices.get(link.getDeviceId());
@NonNull final NetworkPacket identityPacket,
@NonNull final BaseLink link) {
Device device = devices.get(deviceId);
if (device != null) { if (device != null) {
Log.i("KDE/Application", "addLink, known device: " + deviceId); device.addLink(link);
device.addLink(identityPacket, link);
} else { } else {
Log.i("KDE/Application", "addLink,unknown device: " + deviceId); device = new Device(KdeConnect.this, link);
device = new Device(KdeConnect.this, deviceId, certificate, identityPacket, link); devices.put(link.getDeviceId(), device);
devices.put(deviceId, device);
device.addPairingCallback(devicePairingCallback); device.addPairingCallback(devicePairingCallback);
} }
onDeviceListChanged(); onDeviceListChanged();

View File

@ -6,15 +6,10 @@
package org.kde.kdeconnect; package org.kde.kdeconnect;
import android.content.Context;
import android.util.Log;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Plugins.PluginFactory;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -263,26 +258,6 @@ public class NetworkPacket {
return np; return np;
} }
static public NetworkPacket createIdentityPacket(Context context) {
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_IDENTITY);
String deviceId = DeviceHelper.getDeviceId(context);
try {
np.mBody.put("deviceId", deviceId);
np.mBody.put("deviceName", DeviceHelper.getDeviceName(context));
np.mBody.put("protocolVersion", DeviceHelper.ProtocolVersion);
np.mBody.put("deviceType", DeviceHelper.getDeviceType(context).toString());
np.mBody.put("incomingCapabilities", new JSONArray(PluginFactory.getIncomingCapabilities()));
np.mBody.put("outgoingCapabilities", new JSONArray(PluginFactory.getOutgoingCapabilities()));
} catch (Exception e) {
Log.e("NetworkPacket", "Exception on createIdentityPacket", e);
}
return np;
}
public void setPayload(Payload payload) { mPayload = payload; } public void setPayload(Payload payload) { mPayload = payload; }
public Payload getPayload() { public Payload getPayload() {

View File

@ -19,7 +19,7 @@ import android.view.KeyEvent;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.DeviceType;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
@ -33,7 +33,7 @@ public class BigscreenPlugin extends Plugin {
@Override @Override
public boolean isCompatible() { public boolean isCompatible() {
return device.getDeviceType().equals(Device.DeviceType.Tv) && super.isCompatible(); return device.getDeviceType().equals(DeviceType.Tv) && super.isCompatible();
} }
@Override @Override

View File

@ -18,7 +18,7 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.kde.kdeconnect.Device; import org.kde.kdeconnect.DeviceType;
import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect.Plugins.PluginFactory;
@ -41,7 +41,7 @@ public class PresenterPlugin extends Plugin {
@Override @Override
public boolean isCompatible() { public boolean isCompatible() {
return !device.getDeviceType().equals(Device.DeviceType.Phone) && super.isCompatible(); return !device.getDeviceType().equals(DeviceType.Phone) && super.isCompatible();
} }
@Override @Override

View File

@ -92,7 +92,7 @@ class SimpleSftpServer {
sshd.setCommandFactory(new ScpCommandFactory()); sshd.setCommandFactory(new ScpCommandFactory());
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory())); sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystem.Factory()));
keyAuth.deviceKey = device.certificate.getPublicKey(); keyAuth.deviceKey = device.getCertificate().getPublicKey();
sshd.setPublickeyAuthenticator(keyAuth); sshd.setPublickeyAuthenticator(keyAuth);
sshd.setPasswordAuthenticator(passwordAuth); sshd.setPasswordAuthenticator(passwordAuth);

View File

@ -84,7 +84,7 @@ public class DeviceTest {
MockSharedPreference deviceSettings = new MockSharedPreference(); MockSharedPreference deviceSettings = new MockSharedPreference();
SharedPreferences.Editor editor = deviceSettings.edit(); SharedPreferences.Editor editor = deviceSettings.edit();
editor.putString("deviceName", name); editor.putString("deviceName", name);
editor.putString("deviceType", Device.DeviceType.Phone.toString()); editor.putString("deviceType", DeviceType.Phone.toString());
editor.putString("certificate", encodedCertificate); editor.putString("certificate", encodedCertificate);
editor.apply(); editor.apply();
Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings); Mockito.when(context.getSharedPreferences(eq(deviceId), eq(Context.MODE_PRIVATE))).thenReturn(deviceSettings);
@ -110,12 +110,11 @@ public class DeviceTest {
@Test @Test
public void testDeviceType() { public void testDeviceType() {
assertEquals(Device.DeviceType.Phone, Device.DeviceType.FromString(Device.DeviceType.Phone.toString())); assertEquals(DeviceType.Phone, DeviceType.fromString(DeviceType.Phone.toString()));
assertEquals(Device.DeviceType.Tablet, Device.DeviceType.FromString(Device.DeviceType.Tablet.toString())); assertEquals(DeviceType.Tablet, DeviceType.fromString(DeviceType.Tablet.toString()));
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(Device.DeviceType.Computer.toString())); assertEquals(DeviceType.Computer, DeviceType.fromString(DeviceType.Computer.toString()));
assertEquals(Device.DeviceType.Tv, Device.DeviceType.FromString(Device.DeviceType.Tv.toString())); assertEquals(DeviceType.Tv, DeviceType.fromString(DeviceType.Tv.toString()));
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString("")); assertEquals(DeviceType.Computer, DeviceType.fromString("invalid"));
assertEquals(Device.DeviceType.Computer, Device.DeviceType.FromString(null));
} }
// Basic paired device testing // Basic paired device testing
@ -124,10 +123,10 @@ public class DeviceTest {
Device device = new Device(context, "testDevice"); Device device = new Device(context, "testDevice");
assertEquals(device.getDeviceId(), "testDevice"); assertEquals(device.getDeviceId(), "testDevice");
assertEquals(device.getDeviceType(), Device.DeviceType.Phone); assertEquals(device.getDeviceType(), DeviceType.Phone);
assertEquals(device.getName(), "Test Device"); assertEquals(device.getName(), "Test Device");
assertTrue(device.isPaired()); assertTrue(device.isPaired());
assertNotNull(device.certificate); assertNotNull(device.deviceInfo.certificate);
} }
public void testPairingDone() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, CertificateException { public void testPairingDone() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, CertificateException {
@ -137,7 +136,7 @@ public class DeviceTest {
fakeNetworkPacket.set("deviceId", deviceId); fakeNetworkPacket.set("deviceId", deviceId);
fakeNetworkPacket.set("deviceName", "Unpaired Test Device"); fakeNetworkPacket.set("deviceName", "Unpaired Test Device");
fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion); fakeNetworkPacket.set("protocolVersion", DeviceHelper.ProtocolVersion);
fakeNetworkPacket.set("deviceType", Device.DeviceType.Phone.toString()); fakeNetworkPacket.set("deviceType", DeviceType.Phone.toString());
String certificateString = String certificateString =
"MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" + "MIIDVzCCAj+gAwIBAgIBCjANBgkqhkiG9w0BAQUFADBVMS8wLQYDVQQDDCZfZGExNzlhOTFfZjA2\n" +
"NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" + "NF80NzhlX2JlOGNfMTkzNWQ3NTQ0ZDU0XzEMMAoGA1UECgwDS0RFMRQwEgYDVQQLDAtLZGUgY29u\n" +
@ -157,18 +156,21 @@ public class DeviceTest {
"7n+KOQ=="; "7n+KOQ==";
byte[] certificateBytes = Base64.decode(certificateString, 0); byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes); Certificate certificate = SslHelper.parseCertificate(certificateBytes);
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(fakeNetworkPacket, certificate);
LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class); LanLinkProvider linkProvider = Mockito.mock(LanLinkProvider.class);
Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider"); Mockito.when(linkProvider.getName()).thenReturn("LanLinkProvider");
LanLink link = Mockito.mock(LanLink.class); LanLink link = Mockito.mock(LanLink.class);
Mockito.when(link.getLinkProvider()).thenReturn(linkProvider); Mockito.when(link.getLinkProvider()).thenReturn(linkProvider);
Device device = new Device(context, deviceId, certificate, fakeNetworkPacket, link); Mockito.when(link.getDeviceId()).thenReturn(deviceId);
Mockito.when(link.getDeviceInfo()).thenReturn(deviceInfo);
Device device = new Device(context, link);
assertNotNull(device); assertNotNull(device);
assertEquals(device.getDeviceId(), deviceId); assertEquals(device.getDeviceId(), deviceId);
assertEquals(device.getName(), "Unpaired Test Device"); assertEquals(device.getName(), "Unpaired Test Device");
assertEquals(device.getDeviceType(), Device.DeviceType.Phone); assertEquals(device.getDeviceType(), DeviceType.Phone);
assertNotNull(device.certificate); assertNotNull(device.deviceInfo.certificate);
Method method = PairingHandler.class.getDeclaredMethod("pairingDone"); Method method = PairingHandler.class.getDeclaredMethod("pairingDone");
method.setAccessible(true); method.setAccessible(true);

View File

@ -10,10 +10,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import android.content.Context;
import android.util.Log; import android.util.Log;
import org.json.JSONException; import org.json.JSONException;
@ -22,10 +19,13 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.internal.util.collections.Sets;
import org.powermock.api.mockito.PowerMockito; import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.modules.junit4.PowerMockRunner;
import java.security.cert.Certificate;
@RunWith(PowerMockRunner.class) @RunWith(PowerMockRunner.class)
@PrepareForTest({DeviceHelper.class, Log.class}) @PrepareForTest({DeviceHelper.class, Log.class})
public class NetworkPacketTest { public class NetworkPacketTest {
@ -34,7 +34,7 @@ public class NetworkPacketTest {
public void setUp() { public void setUp() {
PowerMockito.mockStatic(DeviceHelper.class); PowerMockito.mockStatic(DeviceHelper.class);
PowerMockito.when(DeviceHelper.getDeviceId(any())).thenReturn("123"); PowerMockito.when(DeviceHelper.getDeviceId(any())).thenReturn("123");
PowerMockito.when(DeviceHelper.getDeviceType(any())).thenReturn(Device.DeviceType.Phone); PowerMockito.when(DeviceHelper.getDeviceType(any())).thenReturn(DeviceType.Phone);
PowerMockito.mockStatic(Log.class); PowerMockito.mockStatic(Log.class);
} }
@ -70,14 +70,23 @@ public class NetworkPacketTest {
@Test @Test
public void testIdentity() { public void testIdentity() {
Certificate cert = Mockito.mock(Certificate.class);
Context context = Mockito.mock(Context.class); DeviceInfo deviceInfo = new DeviceInfo("myid", cert, "myname", DeviceType.Tv, 12, Sets.newSet("ASDFG"), Sets.newSet("QWERTY"));
MockSharedPreference settings = new MockSharedPreference();
Mockito.when(context.getSharedPreferences(anyString(), anyInt())).thenReturn(settings);
NetworkPacket np = NetworkPacket.createIdentityPacket(context); NetworkPacket np = deviceInfo.toIdentityPacket();
assertEquals(np.getInt("protocolVersion"), 12);
DeviceInfo parsed = DeviceInfo.fromIdentityPacketAndCert(np, cert);
assertEquals(parsed.name, deviceInfo.name);
assertEquals(parsed.id, deviceInfo.id);
assertEquals(parsed.type, deviceInfo.type);
assertEquals(parsed.protocolVersion, deviceInfo.protocolVersion);
assertEquals(parsed.incomingCapabilities, deviceInfo.incomingCapabilities);
assertEquals(parsed.outgoingCapabilities, deviceInfo.outgoingCapabilities);
assertEquals(np.getInt("protocolVersion"), DeviceHelper.ProtocolVersion);
} }