Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Add Bluetooth support

This is a ported version of the patch submited in review:
https://git.reviewboard.kde.org/r/128270

It is disabled by default.

REVIEW: 128270
parent 5d7b0a97
......@@ -18,6 +18,8 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<!--<uses-permission android:name="android.permission.BLUETOOTH" />-->
<!--<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
......
/*
* Copyright 2016 Saikrishna Arcot <saiarcot895@gmail.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.BluetoothBackend;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import org.json.JSONException;
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.RsaHelper;
import org.kde.kdeconnect.NetworkPackage;
import java.io.*;
import java.nio.charset.Charset;
import java.security.PublicKey;
import java.util.UUID;
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class BluetoothLink extends BaseLink {
private final BluetoothSocket socket;
private final BluetoothLinkProvider linkProvider;
private boolean continueAccepting = true;
private Thread receivingThread = new Thread(new Runnable() {
@Override
public void run() {
StringBuilder sb = new StringBuilder();
try {
Reader reader = new InputStreamReader(socket.getInputStream(), "UTF-8");
char[] buf = new char[512];
while (continueAccepting) {
while (sb.indexOf("\n") == -1 && continueAccepting) {
int charsRead;
if ((charsRead = reader.read(buf)) > 0) {
sb.append(buf, 0, charsRead);
}
}
int endIndex = sb.indexOf("\n");
if (endIndex != -1) {
String message = sb.substring(0, endIndex + 1);
sb.delete(0, endIndex + 1);
processMessage(message);
}
}
} catch (IOException e) {
Log.e("BluetoothLink/receiving", "Connection to " + socket.getRemoteDevice().getAddress() + " likely broken.", e);
disconnect();
}
}
private void processMessage(String message) {
NetworkPackage np;
try {
np = NetworkPackage.unserialize(message);
} catch (JSONException e) {
Log.e("BluetoothLink/receiving", "Unable to parse message.", e);
return;
}
if (np.getType().equals(NetworkPackage.PACKAGE_TYPE_ENCRYPTED)) {
try {
np = RsaHelper.decrypt(np, privateKey);
} catch(Exception e) {
Log.e("BluetoothLink/receiving", "Exception decrypting the package", e);
}
}
if (np.hasPayloadTransferInfo()) {
BluetoothSocket transferSocket = null;
try {
UUID transferUuid = UUID.fromString(np.getPayloadTransferInfo().getString("uuid"));
transferSocket = socket.getRemoteDevice().createRfcommSocketToServiceRecord(transferUuid);
transferSocket.connect();
np.setPayload(transferSocket.getInputStream(), np.getPayloadSize());
} catch (Exception e) {
if (transferSocket != null) {
try { transferSocket.close(); } catch(IOException ignored) { }
}
Log.e("BluetoothLink/receiving", "Unable to get payload", e);
}
}
packageReceived(np);
}
});
public BluetoothLink(Context context, BluetoothSocket socket, String deviceId, BluetoothLinkProvider linkProvider) {
super(context, deviceId, linkProvider);
this.socket = socket;
this.linkProvider = linkProvider;
receivingThread.start();
}
@Override
public String getName() {
return "BluetoothLink";
}
@Override
public BasePairingHandler getPairingHandler(Device device, BasePairingHandler.PairingHandlerCallback callback) {
return new BluetoothPairingHandler(device, callback);
}
public void disconnect() {
if (socket == null) {
return;
}
continueAccepting = false;
try {
socket.close();
} catch (IOException e) {
}
linkProvider.disconnectedLink(this, getDeviceId(), socket);
}
private void sendMessage(NetworkPackage np) throws JSONException, IOException {
byte[] message = np.serialize().getBytes(Charset.forName("UTF-8"));
OutputStream socket = this.socket.getOutputStream();
Log.i("BluetoothLink","Beginning to send message");
socket.write(message);
Log.i("BluetoothLink","Finished sending message");
}
@Override
public boolean sendPackage(NetworkPackage np, Device.SendPackageStatusCallback callback) {
return sendPackageInternal(np, callback, null);
}
@Override
public boolean sendPackageEncrypted(NetworkPackage np, Device.SendPackageStatusCallback callback, PublicKey key) {
return sendPackageInternal(np, callback, key);
}
private boolean sendPackageInternal(NetworkPackage np, final Device.SendPackageStatusCallback callback, PublicKey key) {
/*if (!isConnected()) {
Log.e("BluetoothLink", "sendPackageEncrypted failed: not connected");
callback.sendFailure(new Exception("Not connected"));
return;
}*/
try {
BluetoothServerSocket serverSocket = null;
if (np.hasPayload()) {
UUID transferUuid = UUID.randomUUID();
serverSocket = BluetoothAdapter.getDefaultAdapter()
.listenUsingRfcommWithServiceRecord("KDE Connect Transfer", transferUuid);
JSONObject payloadTransferInfo = new JSONObject();
payloadTransferInfo.put("uuid", transferUuid.toString());
np.setPayloadTransferInfo(payloadTransferInfo);
}
if (key != null) {
try {
np = RsaHelper.encrypt(np, key);
} catch (Exception e) {
callback.onFailure(e);
return false;
}
}
sendMessage(np);
if (serverSocket != null) {
BluetoothSocket transferSocket = serverSocket.accept();
try {
serverSocket.close();
int idealBufferLength = 4096;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
idealBufferLength = transferSocket.getMaxReceivePacketSize();
}
byte[] buffer = new byte[idealBufferLength];
int bytesRead;
long progress = 0;
InputStream stream = np.getPayload();
while ((bytesRead = stream.read(buffer)) != -1) {
progress += bytesRead;
transferSocket.getOutputStream().write(buffer, 0, bytesRead);
if (np.getPayloadSize() > 0) {
callback.onProgressChanged((int) (100 * progress / np.getPayloadSize()));
}
}
transferSocket.getOutputStream().flush();
stream.close();
} catch (Exception e) {
callback.onFailure(e);
return false;
} finally {
try { transferSocket.close(); } catch (IOException ignored) { }
}
}
callback.onSuccess();
return true;
} catch (Exception e) {
callback.onFailure(e);
return false;
}
}
@Override
public boolean linkShouldBeKeptAlive() {
return receivingThread.isAlive();
}
/*
public boolean isConnected() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
return socket.isConnected();
} else {
return true;
}
}
*/
}
/*
* Copyright 2015 Vineet Garg <grg.vineet@gmail.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.BluetoothBackend;
import android.util.Log;
import org.kde.kdeconnect.Backends.BasePairingHandler;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPackage;
import org.kde.kdeconnect_tp.R;
import java.util.Timer;
import java.util.TimerTask;
public class BluetoothPairingHandler extends BasePairingHandler {
Timer mPairingTimer;
public BluetoothPairingHandler(Device device, final PairingHandlerCallback callback) {
super(device, callback);
if (device.isPaired()) {
mPairStatus = PairStatus.Paired;
} else {
mPairStatus = PairStatus.NotPaired;
}
}
// @Override
public NetworkPackage createPairPackage() {
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", true);
return np;
}
@Override
public void packageReceived(NetworkPackage np) throws Exception{
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.SendPackageStatusCallback statusCallback = new Device.SendPackageStatusCallback() {
@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) {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_could_not_send_package));
}
};
mDevice.sendPackage(createPairPackage(), statusCallback);
}
public void hidePairingNotification() {
mDevice.hidePairingNotification();
if (mPairingTimer != null) {
mPairingTimer .cancel();
}
}
@Override
public void acceptPairing() {
hidePairingNotification();
Device.SendPackageStatusCallback statusCallback = new Device.SendPackageStatusCallback() {
@Override
public void onSuccess() {
pairingDone();
}
@Override
public void onFailure(Throwable e) {
mCallback.pairingFailed(mDevice.getContext().getString(R.string.error_not_reachable));
}
};
mDevice.sendPackage(createPairPackage(), statusCallback);
}
@Override
public void rejectPairing() {
hidePairingNotification();
mPairStatus = PairStatus.NotPaired;
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPackage(np);
}
//@Override
public void pairingDone() {
// Store device information needed to create a Device object in a future
//Log.e("KDE/PairingDone", "Pairing Done");
mPairStatus = PairStatus.Paired;
mCallback.pairingDone();
}
@Override
public void unpair() {
mPairStatus = PairStatus.NotPaired;
NetworkPackage np = new NetworkPackage(NetworkPackage.PACKAGE_TYPE_PAIR);
np.set("pair", false);
mDevice.sendPackage(np);
}
}
......@@ -26,11 +26,13 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
//import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
......@@ -140,11 +142,9 @@ public class BackgroundService extends Service {
}
private void registerLinkProviders() {
//linkProviders.add(new LoopbackLinkProvider(this));
linkProviders.add(new LanLinkProvider(this));
// linkProviders.add(new LoopbackLinkProvider(this));
// linkProviders.add(new BluetoothLinkProvider(this));
}
public ArrayList<BaseLinkProvider> getLinkProviders() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment