mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 18:07:55 +00:00
401 lines
14 KiB
Java
401 lines
14 KiB
Java
/*
|
|
* SPDX-FileCopyrightText: 2016 Saikrishna Arcot <saiarcot895@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
|
*/
|
|
|
|
package org.kde.kdeconnect.Backends.BluetoothBackend;
|
|
|
|
import android.bluetooth.BluetoothAdapter;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.bluetooth.BluetoothServerSocket;
|
|
import android.bluetooth.BluetoothSocket;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.os.Parcelable;
|
|
import android.util.Base64;
|
|
import android.util.Log;
|
|
|
|
import org.kde.kdeconnect.Backends.BaseLinkProvider;
|
|
import org.kde.kdeconnect.Device;
|
|
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
|
|
import org.kde.kdeconnect.Helpers.ThreadHelper;
|
|
import org.kde.kdeconnect.NetworkPacket;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStream;
|
|
import java.io.Reader;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.CertificateException;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
import kotlin.text.Charsets;
|
|
|
|
public class BluetoothLinkProvider extends BaseLinkProvider {
|
|
|
|
private static final UUID SERVICE_UUID = UUID.fromString("185f3df4-3268-4e3f-9fca-d4d5059915bd");
|
|
private static final int REQUEST_ENABLE_BT = 48;
|
|
|
|
private final Context context;
|
|
private final Map<String, BluetoothLink> visibleDevices = new HashMap<>();
|
|
private final Map<BluetoothDevice, BluetoothSocket> sockets = new HashMap<>();
|
|
|
|
private final BluetoothAdapter bluetoothAdapter;
|
|
|
|
private ServerRunnable serverRunnable;
|
|
private ClientRunnable clientRunnable;
|
|
|
|
private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException {
|
|
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);
|
|
BluetoothLink oldLink = visibleDevices.get(deviceId);
|
|
if (oldLink == link) {
|
|
Log.e("BluetoothLinkProvider", "oldLink == link. This should not happen!");
|
|
return;
|
|
}
|
|
visibleDevices.put(deviceId, link);
|
|
onConnectionReceived(deviceId, certificate, identityPacket, link);
|
|
link.startListening();
|
|
if (oldLink != null) {
|
|
Log.i("BluetoothLinkProvider", "Removing old connection to same device");
|
|
oldLink.disconnect();
|
|
}
|
|
}
|
|
|
|
public BluetoothLinkProvider(Context context) {
|
|
this.context = context;
|
|
|
|
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
|
if (bluetoothAdapter == null) {
|
|
Log.e("BluetoothLinkProvider", "No bluetooth adapter found.");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
if (bluetoothAdapter == null) {
|
|
return;
|
|
}
|
|
|
|
if (!bluetoothAdapter.isEnabled()) {
|
|
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
|
Log.e("BluetoothLinkProvider", "Bluetooth adapter not enabled.");
|
|
// TODO: next line needs to be called from an existing activity, so move it?
|
|
// startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
|
|
// TODO: Check result of the previous command, whether the user allowed bluetooth or not.
|
|
return;
|
|
}
|
|
|
|
//This handles the case when I'm the existing device in the network and receive a hello package
|
|
clientRunnable = new ClientRunnable();
|
|
ThreadHelper.execute(clientRunnable);
|
|
|
|
// I'm on a new network, let's be polite and introduce myself
|
|
serverRunnable = new ServerRunnable();
|
|
ThreadHelper.execute(serverRunnable);
|
|
}
|
|
|
|
@Override
|
|
public void onNetworkChange() {
|
|
onStop();
|
|
onStart();
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
if (bluetoothAdapter == null || clientRunnable == null || serverRunnable == null) {
|
|
return;
|
|
}
|
|
|
|
clientRunnable.stopProcessing();
|
|
serverRunnable.stopProcessing();
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "BluetoothLinkProvider";
|
|
}
|
|
|
|
public void disconnectedLink(BluetoothLink link, String deviceId, BluetoothDevice remoteAddress) {
|
|
sockets.remove(remoteAddress);
|
|
visibleDevices.remove(deviceId);
|
|
onConnectionLost(link);
|
|
}
|
|
|
|
private class ServerRunnable implements Runnable {
|
|
|
|
private boolean continueProcessing = true;
|
|
private BluetoothServerSocket serverSocket;
|
|
|
|
void stopProcessing() {
|
|
continueProcessing = false;
|
|
if (serverSocket != null) {
|
|
try {
|
|
serverSocket.close();
|
|
} catch (IOException e) {
|
|
Log.e("KDEConnect", "Exception", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
serverSocket = bluetoothAdapter
|
|
.listenUsingRfcommWithServiceRecord("KDE Connect", SERVICE_UUID);
|
|
} catch (IOException e) {
|
|
Log.e("KDEConnect", "Exception", e);
|
|
return;
|
|
}
|
|
|
|
while (continueProcessing) {
|
|
try {
|
|
BluetoothSocket socket = serverSocket.accept();
|
|
connect(socket);
|
|
} catch (Exception e) {
|
|
Log.e("BTLinkProvider/Server", "Bluetooth error", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void connect(BluetoothSocket socket) throws Exception {
|
|
synchronized (sockets) {
|
|
if (sockets.containsKey(socket.getRemoteDevice())) {
|
|
Log.i("BTLinkProvider/Server", "Received duplicate connection from " + socket.getRemoteDevice().getAddress());
|
|
socket.close();
|
|
return;
|
|
} else {
|
|
sockets.put(socket.getRemoteDevice(), socket);
|
|
}
|
|
}
|
|
|
|
Log.i("BTLinkProvider/Server", "Received connection from " + socket.getRemoteDevice().getAddress());
|
|
|
|
//Delay to let bluetooth initialize stuff correctly
|
|
try {
|
|
Thread.sleep(500);
|
|
} catch (Exception e) {
|
|
synchronized (sockets) {
|
|
sockets.remove(socket.getRemoteDevice());
|
|
}
|
|
throw e;
|
|
}
|
|
|
|
try (ConnectionMultiplexer connection = new ConnectionMultiplexer(socket)) {
|
|
OutputStream outputStream = connection.getDefaultOutputStream();
|
|
InputStream inputStream = connection.getDefaultInputStream();
|
|
|
|
NetworkPacket np = NetworkPacket.createIdentityPacket(context);
|
|
np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
|
|
byte[] message = np.serialize().getBytes(Charsets.UTF_8);
|
|
outputStream.write(message);
|
|
outputStream.flush();
|
|
|
|
Log.i("BTLinkProvider/Server", "Sent identity packet");
|
|
|
|
// Listen for the response
|
|
StringBuilder sb = new StringBuilder();
|
|
Reader reader = new InputStreamReader(inputStream, Charsets.UTF_8);
|
|
int charsRead;
|
|
char[] buf = new char[512];
|
|
while (sb.lastIndexOf("\n") == -1 && (charsRead = reader.read(buf)) != -1) {
|
|
sb.append(buf, 0, charsRead);
|
|
}
|
|
|
|
String response = sb.toString();
|
|
final NetworkPacket identityPacket = NetworkPacket.unserialize(response);
|
|
|
|
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
|
|
Log.e("BTLinkProvider/Server", "2 Expecting an identity packet");
|
|
return;
|
|
}
|
|
|
|
Log.i("BTLinkProvider/Server", "Received identity packet");
|
|
|
|
BluetoothLink link = new BluetoothLink(context, connection,
|
|
inputStream, outputStream, socket.getRemoteDevice(),
|
|
identityPacket.getString("deviceId"), BluetoothLinkProvider.this);
|
|
addLink(identityPacket, link);
|
|
} catch (Exception e) {
|
|
synchronized (sockets) {
|
|
sockets.remove(socket.getRemoteDevice());
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ClientRunnable extends BroadcastReceiver implements Runnable {
|
|
|
|
private boolean continueProcessing = true;
|
|
private final Map<BluetoothDevice, Thread> connectionThreads = new HashMap<>();
|
|
|
|
void stopProcessing() {
|
|
continueProcessing = false;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_UUID);
|
|
context.registerReceiver(this, filter);
|
|
|
|
if (continueProcessing) {
|
|
connectToDevices();
|
|
try {
|
|
Thread.sleep(15000);
|
|
} catch (InterruptedException ignored) {
|
|
}
|
|
}
|
|
|
|
context.unregisterReceiver(this);
|
|
}
|
|
|
|
private void connectToDevices() {
|
|
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
|
|
Log.i("BluetoothLinkProvider", "Bluetooth adapter paired devices: " + pairedDevices.size());
|
|
|
|
// Loop through paired devices
|
|
for (BluetoothDevice device : pairedDevices) {
|
|
if (sockets.containsKey(device)) {
|
|
continue;
|
|
}
|
|
|
|
device.fetchUuidsWithSdp();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
if (BluetoothDevice.ACTION_UUID.equals(action)) {
|
|
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
|
Parcelable[] activeUuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
|
|
|
|
if (sockets.containsKey(device)) {
|
|
return;
|
|
}
|
|
|
|
if (activeUuids == null) {
|
|
return;
|
|
}
|
|
|
|
for (Parcelable uuid : activeUuids) {
|
|
if (uuid.toString().equals(SERVICE_UUID.toString())) {
|
|
connectToDevice(device);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void connectToDevice(BluetoothDevice device) {
|
|
if (!connectionThreads.containsKey(device) || !connectionThreads.get(device).isAlive()) {
|
|
Thread connectionThread = new Thread(new ClientConnect(device));
|
|
connectionThread.start();
|
|
connectionThreads.put(device, connectionThread);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private class ClientConnect implements Runnable {
|
|
|
|
private final BluetoothDevice device;
|
|
|
|
ClientConnect(BluetoothDevice device) {
|
|
this.device = device;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
connectToDevice();
|
|
}
|
|
|
|
private void connectToDevice() {
|
|
BluetoothSocket socket;
|
|
try {
|
|
socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID);
|
|
socket.connect();
|
|
sockets.put(device, socket);
|
|
} catch (IOException e) {
|
|
Log.e("BTLinkProvider/Client", "Could not connect to KDE Connect service on " + device.getAddress(), e);
|
|
return;
|
|
}
|
|
|
|
Log.i("BTLinkProvider/Client", "Connected to " + device.getAddress());
|
|
|
|
try {
|
|
//Delay to let bluetooth initialize stuff correctly
|
|
Thread.sleep(500);
|
|
ConnectionMultiplexer connection = new ConnectionMultiplexer(socket);
|
|
OutputStream outputStream = connection.getDefaultOutputStream();
|
|
InputStream inputStream = connection.getDefaultInputStream();
|
|
|
|
int character;
|
|
StringBuilder sb = new StringBuilder();
|
|
while (sb.lastIndexOf("\n") == -1 && (character = inputStream.read()) != -1) {
|
|
sb.append((char) character);
|
|
}
|
|
|
|
String message = sb.toString();
|
|
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
|
|
|
|
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
|
|
Log.e("BTLinkProvider/Client", "1 Expecting an identity packet");
|
|
socket.close();
|
|
return;
|
|
}
|
|
|
|
Log.i("BTLinkProvider/Client", "Received identity packet");
|
|
|
|
String myId = NetworkPacket.createIdentityPacket(context).getString("deviceId");
|
|
if (identityPacket.getString("deviceId").equals(myId)) {
|
|
// Probably won't happen, but just to be safe
|
|
connection.close();
|
|
return;
|
|
}
|
|
|
|
if (visibleDevices.containsKey(identityPacket.getString("deviceId"))) {
|
|
return;
|
|
}
|
|
|
|
Log.i("BTLinkProvider/Client", "identity packet received, creating link");
|
|
|
|
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream,
|
|
socket.getRemoteDevice(), identityPacket.getString("deviceId"), BluetoothLinkProvider.this);
|
|
|
|
NetworkPacket np2 = NetworkPacket.createIdentityPacket(context);
|
|
link.sendPacket(np2, new Device.SendPacketStatusCallback() {
|
|
@Override
|
|
public void onSuccess() {
|
|
try {
|
|
addLink(identityPacket, link);
|
|
} catch (CertificateException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable e) {
|
|
|
|
}
|
|
}, true);
|
|
} catch (Exception e) {
|
|
Log.e("BTLinkProvider/Client", "Connection lost/disconnected on " + device.getAddress(), e);
|
|
}
|
|
}
|
|
}
|
|
}
|