2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-29 21:27:40 +00:00

Duplicate LanBackend as MulticastBackend

This commit is contained in:
Simon Redman 2019-07-20 12:29:01 +02:00 committed by Albert Vaca Cintora
parent 0cea53540e
commit 58de1aa868
3 changed files with 932 additions and 0 deletions

View File

@ -0,0 +1,268 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
* Copyright 2019 Simon Redman <simon@ergotech.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Backends.MulticastBackend;
import android.content.Context;
import android.util.Log;
import org.json.JSONObject;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.StringsHelper;
import org.kde.kdeconnect.NetworkPacket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.channels.NotYetConnectedException;
import javax.net.ssl.SSLSocket;
public class MulticastLink extends BaseLink {
static final String LOG_TAG = "MulticastLink";
public interface LinkDisconnectedCallback {
void linkDisconnected(MulticastLink brokenLink);
}
public enum ConnectionStarted {
Locally, Remotely
}
private ConnectionStarted connectionSource; // If the other device sent me a broadcast,
// I should not close the connection with it
// because it's probably trying to find me and
// potentially ask for pairing.
private volatile SSLSocket socket = null;
private final LinkDisconnectedCallback callback;
@Override
public void disconnect() {
Log.i(LOG_TAG + "Disconnect","socket:"+ socket.hashCode());
try {
socket.close();
} catch (IOException e) {
Log.e(LOG_TAG, "Error", e);
}
}
//Returns the old socket
public SSLSocket reset(final SSLSocket newSocket, ConnectionStarted connectionSource) throws IOException {
SSLSocket oldSocket = socket;
socket = newSocket;
this.connectionSource = connectionSource;
if (oldSocket != null) {
oldSocket.close(); //This should cancel the readThread
}
//Log.e(LOG_TAG, "Start listening");
//Create a thread to take care of incoming data for the new socket
new Thread(() -> {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), StringsHelper.UTF8));
while (true) {
String packet;
try {
packet = reader.readLine();
} catch (SocketTimeoutException e) {
continue;
}
if (packet == null) {
throw new IOException("End of stream");
}
if (packet.isEmpty()) {
continue;
}
NetworkPacket np = NetworkPacket.unserialize(packet);
receivedNetworkPacket(np);
}
} catch (Exception e) {
Log.i(LOG_TAG, "Socket closed: " + newSocket.hashCode() + ". Reason: " + e.getMessage());
try { Thread.sleep(300); } catch (InterruptedException ignored) {} // Wait a bit because we might receive a new socket meanwhile
boolean thereIsaANewSocket = (newSocket != socket);
if (!thereIsaANewSocket) {
callback.linkDisconnected(MulticastLink.this);
}
}
}).start();
return oldSocket;
}
public MulticastLink(Context context, String deviceId, MulticastLinkProvider linkProvider, SSLSocket socket, ConnectionStarted connectionSource) throws IOException {
super(context, deviceId, linkProvider);
callback = linkProvider;
reset(socket, connectionSource);
}
@Override
public String getName() {
return "LanLink";
}
@Override
public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) {
return new MulticastPairingHandler(device, callback);
}
//Blocking, do not call from main thread
@Override
public boolean sendPacket(NetworkPacket np, final Device.SendPacketStatusCallback callback) {
if (socket == null) {
Log.e(LOG_TAG + "sendPacket", "Not yet connected");
callback.onFailure(new NotYetConnectedException());
return false;
}
try {
//Prepare socket for the payload
final ServerSocket server;
if (np.hasPayload()) {
server = MulticastLinkProvider.openServerSocketOnFreePort(MulticastLinkProvider.PAYLOAD_TRANSFER_MIN_PORT);
JSONObject payloadTransferInfo = new JSONObject();
payloadTransferInfo.put("port", server.getLocalPort());
np.setPayloadTransferInfo(payloadTransferInfo);
} else {
server = null;
}
//Log.e("LanLink/sendPacket", np.getType());
//Send body of the network package
try {
OutputStream writer = socket.getOutputStream();
writer.write(np.serialize().getBytes(StringsHelper.UTF8));
writer.flush();
} catch (Exception e) {
disconnect(); //main socket is broken, disconnect
throw e;
}
//Send payload
if (server != null) {
Socket payloadSocket = null;
OutputStream outputStream = null;
InputStream inputStream;
try {
//Wait a maximum of 10 seconds for the other end to establish a connection with our socket, close it afterwards
server.setSoTimeout(10*1000);
payloadSocket = server.accept();
//Convert to SSL if needed
payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, false);
outputStream = payloadSocket.getOutputStream();
inputStream = np.getPayload().getInputStream();
Log.i(LOG_TAG, "Beginning to send payload");
byte[] buffer = new byte[4096];
int bytesRead;
long size = np.getPayloadSize();
long progress = 0;
long timeSinceLastUpdate = -1;
while (!np.isCanceled() && (bytesRead = inputStream.read(buffer)) != -1) {
//Log.e("ok",""+bytesRead);
progress += bytesRead;
outputStream.write(buffer, 0, bytesRead);
if (size > 0) {
if (timeSinceLastUpdate + 500 < System.currentTimeMillis()) { //Report progress every half a second
long percent = ((100 * progress) / size);
callback.onProgressChanged((int) percent);
timeSinceLastUpdate = System.currentTimeMillis();
}
}
}
outputStream.flush();
Log.i(LOG_TAG, "Finished sending payload ("+progress+" bytes written)");
} finally {
try { server.close(); } catch (Exception ignored) { }
try { payloadSocket.close(); } catch (Exception ignored) { }
np.getPayload().close();
try { outputStream.close(); } catch (Exception ignored) { }
}
}
if (!np.isCanceled()) {
callback.onSuccess();
}
return true;
} catch (Exception e) {
if (callback != null) {
callback.onFailure(e);
}
return false;
} finally {
//Make sure we close the payload stream, if any
if (np.hasPayload()) {
np.getPayload().close();
}
}
}
private void receivedNetworkPacket(NetworkPacket np) {
if (np.hasPayloadTransferInfo()) {
Socket payloadSocket = new Socket();
try {
int tcpPort = np.getPayloadTransferInfo().getInt("port");
InetSocketAddress deviceAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
payloadSocket.connect(new InetSocketAddress(deviceAddress.getAddress(), tcpPort));
payloadSocket = SslHelper.convertToSslSocket(context, payloadSocket, getDeviceId(), true, true);
np.setPayload(new NetworkPacket.Payload(payloadSocket, np.getPayloadSize()));
} catch (Exception e) {
try { payloadSocket.close(); } catch(Exception ignored) { }
Log.e(LOG_TAG, "Exception connecting to payload remote socket", e);
}
}
packageReceived(np);
}
@Override
public boolean linkShouldBeKeptAlive() {
return true; //FIXME: Current implementation is broken, so for now we will keep links always established
//We keep the remotely initiated connections, since the remotes require them if they want to request
//pairing to us, or connections that are already paired.
//return (connectionSource == ConnectionStarted.Remotely);
}
}

View File

@ -0,0 +1,447 @@
/*
* Copyright 2014 Albert Vaca Cintora <albertvaka@gmail.com>
* Copyright 2019 Simon Redman <simon@ergotech.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Backends.MulticastBackend;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.NetworkHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.StringsHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.CustomDevicesActivity;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
/**
* This BaseLinkProvider creates {@link MulticastLink}s to other devices on the same
* WiFi network. The first packet sent over a socket must be an
* {@link NetworkPacket#createIdentityPacket(Context)}.
*
* @see #identityPacketReceived(NetworkPacket, Socket, MulticastLink.ConnectionStarted)
*/
public class MulticastLinkProvider extends BaseLinkProvider implements MulticastLink.LinkDisconnectedCallback {
static final String LOG_TAG = "MulticastLink";
private final static int MIN_PORT = 1716;
private final static int MAX_PORT = 1764;
final static int PAYLOAD_TRANSFER_MIN_PORT = 1739;
private final Context context;
private final HashMap<String, MulticastLink> visibleComputers = new HashMap<>(); //Links by device id
private ServerSocket tcpServer;
private DatagramSocket udpServer;
private boolean listening = false;
// To prevent infinte loop between Android < IceCream because both device can only broadcast identity package but cannot connect via TCP
private final ArrayList<InetAddress> reverseConnectionBlackList = new ArrayList<>();
@Override // SocketClosedCallback
public void linkDisconnected(MulticastLink brokenLink) {
String deviceId = brokenLink.getDeviceId();
visibleComputers.remove(deviceId);
connectionLost(brokenLink);
}
//They received my UDP broadcast and are connecting to me. The first thing they sned should be their identity.
private void tcpPacketReceived(Socket socket) {
NetworkPacket networkPacket;
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message = reader.readLine();
networkPacket = NetworkPacket.unserialize(message);
//Log.e("TcpListener","Received TCP package: "+networkPacket.serialize());
} catch (Exception e) {
Log.e(LOG_TAG, "Exception while receiving TCP packet", e);
return;
}
if (!networkPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
Log.e(LOG_TAG, "Expecting an identity package instead of " + networkPacket.getType());
return;
}
Log.i(LOG_TAG, "Identity package received from a TCP connection from " + networkPacket.getString("deviceName"));
identityPacketReceived(networkPacket, socket, MulticastLink.ConnectionStarted.Locally);
}
//I've received their broadcast and should connect to their TCP socket and send my identity.
private void udpPacketReceived(DatagramPacket packet) {
final InetAddress address = packet.getAddress();
try {
String message = new String(packet.getData(), StringsHelper.UTF8);
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
final String deviceId = identityPacket.getString("deviceId");
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
Log.e(LOG_TAG, "Expecting an UDP identity package");
return;
} else {
String myId = DeviceHelper.getDeviceId(context);
if (deviceId.equals(myId)) {
//Ignore my own broadcast
return;
}
}
Log.i(LOG_TAG, "Broadcast identity package received from " + identityPacket.getString("deviceName"));
int tcpPort = identityPacket.getInt("tcpPort", MIN_PORT);
SocketFactory socketFactory = SocketFactory.getDefault();
Socket socket = socketFactory.createSocket(address, tcpPort);
configureSocket(socket);
OutputStream out = socket.getOutputStream();
NetworkPacket myIdentity = NetworkPacket.createIdentityPacket(context);
out.write(myIdentity.serialize().getBytes());
out.flush();
identityPacketReceived(identityPacket, socket, MulticastLink.ConnectionStarted.Remotely);
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot connect to " + address, e);
if (!reverseConnectionBlackList.contains(address)) {
Log.w(LOG_TAG, "Blacklisting " + address);
reverseConnectionBlackList.add(address);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
reverseConnectionBlackList.remove(address);
}
}, 5 * 1000);
// Try to cause a reverse connection
onNetworkChange();
}
}
}
private void configureSocket(Socket socket) {
try {
socket.setKeepAlive(true);
} catch (SocketException e) {
Log.e(LOG_TAG, "Exception", e);
}
}
/**
* Called when a new 'identity' packet is received. Those are passed here by
* {@link #tcpPacketReceived(Socket)} and {@link #udpPacketReceived(DatagramPacket)}.
* <p>
* If the remote device should be connected, this calls {@link #addLink}.
* Otherwise, if there was an Exception, we unpair from that device.
* </p>
*
* @param identityPacket identity of a remote device
* @param socket a new Socket, which should be used to receive packets from the remote device
* @param connectionStarted which side started this connection
*/
private void identityPacketReceived(final NetworkPacket identityPacket, final Socket socket, final MulticastLink.ConnectionStarted connectionStarted) {
String myId = DeviceHelper.getDeviceId(context);
final String deviceId = identityPacket.getString("deviceId");
if (deviceId.equals(myId)) {
Log.e(LOG_TAG, "Somehow I'm connected to myself, ignoring. This should not happen.");
return;
}
// If I'm the TCP server I will be the SSL client and viceversa.
final boolean clientMode = (connectionStarted == MulticastLink.ConnectionStarted.Locally);
// Do the SSL handshake
try {
SharedPreferences preferences = context.getSharedPreferences("trusted_devices", Context.MODE_PRIVATE);
boolean isDeviceTrusted = preferences.getBoolean(deviceId, false);
if (isDeviceTrusted && !SslHelper.isCertificateStored(context, deviceId)) {
//Device paired with and old version, we can't use it as we lack the certificate
BackgroundService.RunCommand(context, service -> {
Device device = service.getDevice(deviceId);
if (device == null) return;
device.unpair();
//Retry as unpaired
identityPacketReceived(identityPacket, socket, connectionStarted);
});
}
Log.i(LOG_TAG, "Starting SSL handshake with " + identityPacket.getString("deviceName") + " trusted:" + isDeviceTrusted);
final SSLSocket sslsocket = SslHelper.convertToSslSocket(context, socket, deviceId, isDeviceTrusted, clientMode);
sslsocket.addHandshakeCompletedListener(event -> {
String mode = clientMode ? "client" : "server";
try {
Certificate certificate = event.getPeerCertificates()[0];
identityPacket.set("certificate", Base64.encodeToString(certificate.getEncoded(), 0));
Log.i(LOG_TAG, "Handshake as " + mode + " successful with " + identityPacket.getString("deviceName") + " secured with " + event.getCipherSuite());
addLink(identityPacket, sslsocket, connectionStarted);
} catch (Exception e) {
Log.e(LOG_TAG, "Handshake as " + mode + " failed with " + identityPacket.getString("deviceName"), e);
BackgroundService.RunCommand(context, service -> {
Device device = service.getDevice(deviceId);
if (device == null) return;
device.unpair();
});
}
});
//Handshake is blocking, so do it on another thread and free this thread to keep receiving new connection
new Thread(() -> {
try {
synchronized (this) {
sslsocket.startHandshake();
}
} catch (Exception e) {
Log.e(LOG_TAG, "Handshake failed with " + identityPacket.getString("deviceName"), e);
//String[] ciphers = sslsocket.getSupportedCipherSuites();
//for (String cipher : ciphers) {
// Log.i("SupportedCiphers","cipher: " + cipher);
//}
}
}).start();
} catch (Exception e) {
Log.e(LOG_TAG, "Exception", e);
}
}
/**
* Add or update a link in the {@link #visibleComputers} map. This method is synchronized, which ensures that only one
* link is operated on at a time.
* <p>
* Without synchronization, the call to {@link SslHelper#parseCertificate(byte[])} in
* {@link Device#addLink(NetworkPacket, BaseLink)} crashes on some devices running Oreo 8.1 (SDK level 27).
* </p>
*
* @param identityPacket representation of remote device
* @param socket a new Socket, which should be used to receive packets from the remote device
* @param connectionOrigin which side started this connection
* @throws IOException if an exception is thrown by {@link MulticastLink#reset(SSLSocket, MulticastLink.ConnectionStarted)}
*/
private void addLink(final NetworkPacket identityPacket, SSLSocket socket, MulticastLink.ConnectionStarted connectionOrigin) throws IOException {
String deviceId = identityPacket.getString("deviceId");
MulticastLink currentLink = visibleComputers.get(deviceId);
if (currentLink != null) {
//Update old link
Log.i(LOG_TAG, "Reusing same link for device " + deviceId);
final Socket oldSocket = currentLink.reset(socket, connectionOrigin);
//Log.e(LOG_TAG, "Replacing socket. old: "+ oldSocket.hashCode() + " - new: "+ socket.hashCode());
} else {
Log.i(LOG_TAG, "Creating a new link for device " + deviceId);
//Let's create the link
MulticastLink link = new MulticastLink(context, deviceId, this, socket, connectionOrigin);
visibleComputers.put(deviceId, link);
connectionAccepted(identityPacket, link);
}
}
public MulticastLinkProvider(Context context) {
this.context = context;
}
private void setupUdpListener() {
try {
udpServer = new DatagramSocket(MIN_PORT);
udpServer.setReuseAddress(true);
udpServer.setBroadcast(true);
} catch (SocketException e) {
Log.e(LOG_TAG, "Error creating udp server", e);
return;
}
new Thread(() -> {
while (listening) {
final int bufferSize = 1024 * 512;
byte[] data = new byte[bufferSize];
DatagramPacket packet = new DatagramPacket(data, bufferSize);
try {
udpServer.receive(packet);
udpPacketReceived(packet);
} catch (Exception e) {
Log.e(LOG_TAG, "UdpReceive exception", e);
}
}
Log.w("UdpListener", "Stopping UDP listener");
}).start();
}
private void setupTcpListener() {
try {
tcpServer = openServerSocketOnFreePort(MIN_PORT);
} catch (Exception e) {
Log.e(LOG_TAG, "Error creating tcp server", e);
return;
}
new Thread(() -> {
while (listening) {
try {
Socket socket = tcpServer.accept();
configureSocket(socket);
tcpPacketReceived(socket);
} catch (Exception e) {
Log.e(LOG_TAG, "TcpReceive exception", e);
}
}
Log.w("TcpListener", "Stopping TCP listener");
}).start();
}
static ServerSocket openServerSocketOnFreePort(int minPort) throws IOException {
int tcpPort = minPort;
while (tcpPort <= MAX_PORT) {
try {
ServerSocket candidateServer = new ServerSocket();
candidateServer.bind(new InetSocketAddress(tcpPort));
Log.i(LOG_TAG, "Using port " + tcpPort);
return candidateServer;
} catch (IOException e) {
tcpPort++;
if (tcpPort == MAX_PORT) {
Log.e(LOG_TAG, "No ports available");
throw e; //Propagate exception
}
}
}
throw new RuntimeException("This should not be reachable");
}
private void broadcastUdpPacket() {
if (NetworkHelper.isOnMobileNetwork(context)) {
Log.w(LOG_TAG, "On 3G network, not sending broadcast.");
return;
}
new Thread(() -> {
ArrayList<String> iplist = CustomDevicesActivity
.getCustomDeviceList(PreferenceManager.getDefaultSharedPreferences(context));
iplist.add("255.255.255.255"); //Default: broadcast.
NetworkPacket identity = NetworkPacket.createIdentityPacket(context);
int port = (tcpServer == null || !tcpServer.isBound()) ? MIN_PORT : tcpServer.getLocalPort();
identity.set("tcpPort", port);
DatagramSocket socket = null;
byte[] bytes = null;
try {
socket = new DatagramSocket();
socket.setReuseAddress(true);
socket.setBroadcast(true);
bytes = identity.serialize().getBytes(StringsHelper.UTF8);
} catch (Exception e) {
Log.e(LOG_TAG, "Failed to create DatagramSocket", e);
}
if (bytes != null) {
//Log.e(LOG_TAG,"Sending packet to "+iplist.size()+" ips");
for (String ipstr : iplist) {
try {
InetAddress client = InetAddress.getByName(ipstr);
socket.send(new DatagramPacket(bytes, bytes.length, client, MIN_PORT));
//Log.i(LOG_TAG,"Udp identity package sent to address "+client);
} catch (Exception e) {
Log.e(LOG_TAG, "Sending udp identity package failed. Invalid address? (" + ipstr + ")", e);
}
}
}
if (socket != null) {
socket.close();
}
}).start();
}
@Override
public void onStart() {
//Log.i(LOG_TAG, "onStart");
if (!listening) {
listening = true;
setupUdpListener();
setupTcpListener();
broadcastUdpPacket();
}
}
@Override
public void onNetworkChange() {
broadcastUdpPacket();
}
@Override
public void onStop() {
//Log.i(LOG_TAG, "onStop");
listening = false;
try {
tcpServer.close();
} catch (Exception e) {
Log.e(LOG_TAG, "Exception", e);
}
try {
udpServer.close();
} catch (Exception e) {
Log.e(LOG_TAG, "Exception", e);
}
}
@Override
public String getName() {
return "MulticastLinkProvider";
}
}

View File

@ -0,0 +1,217 @@
/*
* Copyright 2015 Vineet Garg <grg.vineet@gmail.com>
* Copyright 2019 Simon Redman <simon@ergotech.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kde.kdeconnect.Backends.MulticastBackend;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
import android.util.Log;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect_tp.R;
import java.security.cert.CertificateEncodingException;
import java.util.Timer;
import java.util.TimerTask;
public class MulticastPairingHandler extends BasePairingHandler {
private Timer mPairingTimer;
public MulticastPairingHandler(Device device, final PairingHandlerCallback callback) {
super(device, callback);
if (device.isPaired()) {
mPairStatus = PairStatus.Paired;
} else {
mPairStatus = PairStatus.NotPaired;
}
}
private NetworkPacket createPairPacket() {
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", true);
return np;
}
@Override
public void packageReceived(NetworkPacket np) {
boolean wantsPair = np.getBoolean("pair");
if (wantsPair == isPaired()) {
if (mPairStatus == PairStatus.Requested) {
//Log.e("Device","Unpairing (pair rejected)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
}
return;
}
if (wantsPair) {
if (mPairStatus == PairStatus.Requested) { //We started pairing
hidePairingNotification();
pairingDone();
} else {
// If device is already paired, accept pairing silently
if (mDevice.isPaired()) {
acceptPairing();
return;
}
// Pairing notifications are still managed by device as there is no other way to
// know about notificationId to cancel notification when PairActivity is started
// Even putting notificationId in intent does not work because PairActivity can be
// started from MainActivity too, so then notificationId cannot be set
hidePairingNotification();
mDevice.displayPairingNotification();
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
Log.w("KDE/Device","Unpairing (timeout B)");
mPairStatus = PairStatus.NotPaired;
hidePairingNotification();
}
}, 25*1000); //Time to show notification, waiting for user to accept (peer will timeout in 30 seconds)
mPairStatus = PairStatus.RequestedByPeer;
mCallback.incomingRequest();
}
} else {
Log.i("KDE/Pairing", "Unpair request");
if (mPairStatus == PairStatus.Requested) {
hidePairingNotification();
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_canceled_by_other_peer));
} else if (mPairStatus == PairStatus.Paired) {
mCallback.unpaired();
}
mPairStatus = PairStatus.NotPaired;
}
}
@Override
public void requestPairing() {
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
hidePairingNotification(); //Will stop the pairingTimer if it was running
mPairingTimer = new Timer();
mPairingTimer.schedule(new TimerTask() {
@Override
public void run() {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_timed_out));
Log.w("KDE/Device","Unpairing (timeout A)");
mPairStatus = PairStatus.NotPaired;
}
}, 30*1000); //Time to wait for the other to accept
mPairStatus = PairStatus.Requested;
}
@Override
public void onFailure(Throwable e) {
Log.e("LanPairing/onFailure", "Exception", e);
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_could_not_send_package));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
private void hidePairingNotification() {
mDevice.hidePairingNotification();
if (mPairingTimer != null) {
mPairingTimer .cancel();
}
}
@Override
public void acceptPairing() {
hidePairingNotification();
Device.SendPacketStatusCallback statusCallback = new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
pairingDone();
}
@Override
public void onFailure(Throwable e) {
Log.e("LanPairing/onFailure", "Exception", e);
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
}
};
mDevice.sendPacket(createPairPacket(), statusCallback);
}
@Override
public void rejectPairing() {
hidePairingNotification();
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
private void pairingDone() {
// Store device information needed to create a Device object in a future
//Log.e("KDE/PairingDone", "Pairing Done");
SharedPreferences.Editor editor = mDevice.getContext().getSharedPreferences(mDevice.getDeviceId(), Context.MODE_PRIVATE).edit();
try {
String encodedCertificate = Base64.encodeToString(mDevice.certificate.getEncoded(), 0);
editor.putString("certificate", encodedCertificate);
} catch (NullPointerException n) {
Log.w("KDE/PairingDone", "Certificate is null, remote device does not support ssl", n);
} catch (CertificateEncodingException c) {
Log.e("KDE/PairingDOne", "Error encoding certificate", c);
} catch (Exception e) {
Log.e("KDE/Pairng", "Exception", e);
}
editor.apply();
mPairStatus = PairStatus.Paired;
mCallback.pairingDone();
}
@Override
public void unpair() {
mPairStatus = PairStatus.NotPaired;
NetworkPacket np = new NetworkPacket(NetworkPacket.PACKET_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPacket(np);
}
}