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

Working bluetooth implementation

This commit is contained in:
Rob Emery 2023-10-26 18:14:59 +01:00
parent 22b6427a18
commit 19ec81eec4
13 changed files with 202 additions and 40 deletions

View File

@ -17,8 +17,10 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<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.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

View File

@ -55,4 +55,5 @@ public abstract class BaseLinkProvider {
public abstract void onNetworkChange(@Nullable Network network);
public abstract String getName();
public abstract int getPriority();
}

View File

@ -189,10 +189,4 @@ public class BluetoothLink extends BaseLink {
return false;
}
}
/*
public boolean isConnected() {
return socket.isConnected();
}
*/
}

View File

@ -14,8 +14,11 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.net.Network;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log;
@ -29,6 +32,7 @@ import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Helpers.ThreadHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.SettingsFragment;
import java.io.IOException;
import java.io.InputStream;
@ -39,6 +43,7 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
@ -58,6 +63,8 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
private ServerRunnable serverRunnable;
private ClientRunnable clientRunnable;
private Random randomiser;
private void addLink(NetworkPacket identityPacket, BluetoothLink link) throws CertificateException {
String deviceId = identityPacket.getString("deviceId");
Log.i("BluetoothLinkProvider", "addLink to " + deviceId);
@ -66,7 +73,9 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.e("BluetoothLinkProvider", "oldLink == link. This should not happen!");
return;
}
synchronized (visibleDevices) {
visibleDevices.put(deviceId, link);
}
onConnectionReceived(link);
link.startListening();
link.packetReceived(identityPacket);
@ -79,6 +88,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
public BluetoothLinkProvider(Context context) {
this.context = context;
this.randomiser = new Random();
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
Log.e("BluetoothLinkProvider", "No bluetooth adapter found.");
@ -87,6 +97,10 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
@Override
public void onStart() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
if (!preferences.getBoolean(SettingsFragment.KEY_BLUETOOTH_ENABLED, false)) {
return;
}
if (bluetoothAdapter == null) {
return;
}
@ -100,6 +114,8 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
return;
}
Log.i("BluetoothLinkProvider", "onStart called");
//This handles the case when I'm the existing device in the network and receive a hello package
clientRunnable = new ClientRunnable();
ThreadHelper.execute(clientRunnable);
@ -111,6 +127,8 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
@Override
public void onNetworkChange(@Nullable Network network) {
Log.i("BluetoothLinkProvider", "onNetworkChange called");
onStop();
onStart();
}
@ -120,7 +138,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
if (bluetoothAdapter == null || clientRunnable == null || serverRunnable == null) {
return;
}
Log.i("BluetoothLinkProvider", "onStop called");
clientRunnable.stopProcessing();
serverRunnable.stopProcessing();
}
@ -130,9 +148,20 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
return "BluetoothLinkProvider";
}
@Override
public int getPriority() {
return 10;
}
public void disconnectedLink(BluetoothLink link, BluetoothDevice remoteAddress) {
Log.i("BluetoothLinkProvider", "disconnectedLink called");
synchronized (sockets) {
sockets.remove(remoteAddress);
}
synchronized (visibleDevices) {
visibleDevices.remove(link.getDeviceId());
}
onConnectionLost(link);
}
@ -158,15 +187,18 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
} catch (IOException e) {
Log.e("KDEConnect", "Exception", e);
return;
} catch (SecurityException e) {
Log.e("KDEConnect", "Security Exception for CONNECT", e);
return;
}
while (continueProcessing) {
try {
while (continueProcessing) {
BluetoothSocket socket = serverSocket.accept();
connect(socket);
} catch (Exception e) {
Log.e("BTLinkProvider/Server", "Bluetooth error", e);
}
} catch (Exception e) {
Log.d("BTLinkProvider/Server", "Bluetooth Server error", e);
}
}
@ -199,6 +231,7 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
DeviceInfo myDeviceInfo = DeviceHelper.getDeviceInfo(context);
NetworkPacket np = myDeviceInfo.toIdentityPacket();
np.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
byte[] message = np.serialize().getBytes(Charsets.UTF_8);
@ -226,29 +259,38 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.i("BTLinkProvider/Server", "Received identity packet");
String certificateString = identityPacket.getString("certificate");
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
String PemEncodedCertificateString = identityPacket.getString("certificate");
String base64CertificateString = PemEncodedCertificateString
.replace("-----BEGIN CERTIFICATE-----\n","")
.replace("-----END CERTIFICATE-----\n","");
byte[] PEMEncodedCertificateBytes = Base64.decode(base64CertificateString, 0);
Certificate certificate = SslHelper.parseCertificate(PEMEncodedCertificateBytes);
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
Log.i("BTLinkProvider/Server", "About to create link");
BluetoothLink link = new BluetoothLink(context, connection,
inputStream, outputStream, socket.getRemoteDevice(),
deviceInfo, BluetoothLinkProvider.this);
Log.i("BTLinkProvider/Server", "About to addLink");
addLink(identityPacket, link);
Log.i("BTLinkProvider/Server", "Link Added");
} catch (Exception e) {
synchronized (sockets) {
sockets.remove(socket.getRemoteDevice());
Log.i("BTLinkProvider/Server", "Exception thrown, removing socket", e);
}
throw e;
}
}
}
public static class ClientRunnableSingleton {
private static Map<BluetoothDevice, Thread> connectionThreads = new HashMap<>();
}
private class ClientRunnable extends BroadcastReceiver implements Runnable {
private boolean continueProcessing = true;
private final Map<BluetoothDevice, Thread> connectionThreads = new HashMap<>();
void stopProcessing() {
continueProcessing = false;
@ -256,31 +298,57 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
@Override
public void run() {
Log.i("ClientRunnable", "run called");
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_UUID);
context.registerReceiver(this, filter);
Log.i("ClientRunnable", "receiver registered");
if (continueProcessing) {
connectToDevices();
Log.i("ClientRunnable", "before connectToDevices");
discoverDeviceServices();
Log.i("ClientRunnable", "after connectToDevices");
try {
Thread.sleep(15000);
} catch (InterruptedException ignored) {
}
}
Log.i("ClientRunnable", "unregisteringReceiver");
context.unregisterReceiver(this);
}
private void connectToDevices() {
/**
* Tell Android to use ServiceDiscoveryProtocol to update the
* list of available UUIDs associated with Bluetooth devices
* that are bluetooth-paired-but-not-yet-kde-paired
*/
private void discoverDeviceServices() {
Log.i("ClientRunnable", "connectToDevices called");
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if(pairedDevices == null) {
return;
}
Log.i("BluetoothLinkProvider", "Bluetooth adapter paired devices: " + pairedDevices.size());
// Loop through paired devices
// Loop through Bluetooth paired devices
for (BluetoothDevice device : pairedDevices) {
// If a socket exists for this, then it has been paired in KDE
if (sockets.containsKey(device)) {
continue;
}
Log.i("ClientRunnable", "Calling fetchUuidsWithSdp for device: "+device);
device.fetchUuidsWithSdp();
ParcelUuid[] deviceUuids = device.getUuids();
if(deviceUuids != null){
for(ParcelUuid thisUuid : deviceUuids) {
Log.i("ClientRunnable", "device "+device + " uuid: "+ thisUuid);
}
}
}
}
@ -309,14 +377,14 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
}
private void connectToDevice(BluetoothDevice device) {
if (!connectionThreads.containsKey(device) || !connectionThreads.get(device).isAlive()) {
synchronized (ClientRunnableSingleton.connectionThreads) {
if (!ClientRunnableSingleton.connectionThreads.containsKey(device) || !ClientRunnableSingleton.connectionThreads.get(device).isAlive()) {
Thread connectionThread = new Thread(new ClientConnect(device));
connectionThread.start();
connectionThreads.put(device, connectionThread);
ClientRunnableSingleton.connectionThreads.put(device, connectionThread);
}
}
}
}
private class ClientConnect implements Runnable {
@ -335,12 +403,21 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
private void connectToDevice() {
BluetoothSocket socket;
try {
Log.i("BTLinkProvider/Client", "Cancelling Discovery");
bluetoothAdapter.cancelDiscovery();
Log.i("BTLinkProvider/Client", "Creating RFCommSocket to Service Record");
socket = device.createRfcommSocketToServiceRecord(SERVICE_UUID);
Log.i("BTLinkProvider/Client", "Connecting to ServiceRecord Socket");
socket.connect();
synchronized (sockets) {
sockets.put(device, socket);
}
} catch (IOException e) {
Log.e("BTLinkProvider/Client", "Could not connect to KDE Connect service on " + device.getAddress(), e);
return;
} catch (SecurityException e){
Log.e("BTLinkProvider/Client", "Security Exception connecting to " + device.getAddress(), e);
return;
}
Log.i("BTLinkProvider/Client", "Connected to " + device.getAddress());
@ -348,19 +425,25 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
try {
//Delay to let bluetooth initialize stuff correctly
Thread.sleep(500);
ConnectionMultiplexer connection = new ConnectionMultiplexer(socket);
OutputStream outputStream = connection.getDefaultOutputStream();
InputStream inputStream = connection.getDefaultInputStream();
Log.i("BTLinkProvider/Client", "Device: " + device.getAddress() +" Before inputStream.read()");
int character;
StringBuilder sb = new StringBuilder();
while (sb.lastIndexOf("\n") == -1 && (character = inputStream.read()) != -1) {
sb.append((char) character);
}
Log.i("BTLinkProvider/Client", "Device: " + device.getAddress() +" Before sb.toString()");
String message = sb.toString();
Log.i("BTLinkProvider/Client", "Device: " + device.getAddress() +" Before unserialize (message: '"+message+"')");
final NetworkPacket identityPacket = NetworkPacket.unserialize(message);
Log.i("BTLinkProvider/Client", "Device: " + device.getAddress() +" After unserialize");
if (!identityPacket.getType().equals(NetworkPacket.PACKET_TYPE_IDENTITY)) {
Log.e("BTLinkProvider/Client", "1 Expecting an identity packet");
socket.close();
@ -382,9 +465,12 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
Log.i("BTLinkProvider/Client", "identity packet received, creating link");
String certificateString = identityPacket.getString("certificate");
byte[] certificateBytes = Base64.decode(certificateString, 0);
Certificate certificate = SslHelper.parseCertificate(certificateBytes);
String PemEncodedCertificateString = identityPacket.getString("certificate");
String base64CertificateString = PemEncodedCertificateString
.replace("-----BEGIN CERTIFICATE-----\n","")
.replace("-----END CERTIFICATE-----\n","");
byte[] PEMEncodedCertificateBytes = Base64.decode(base64CertificateString, 0);
Certificate certificate = SslHelper.parseCertificate(PEMEncodedCertificateBytes);
DeviceInfo deviceInfo = DeviceInfo.fromIdentityPacketAndCert(identityPacket, certificate);
final BluetoothLink link = new BluetoothLink(context, connection, inputStream, outputStream,
@ -394,6 +480,8 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
NetworkPacket np2 = myDeviceInfo.toIdentityPacket();
np2.set("certificate", Base64.encodeToString(SslHelper.certificate.getEncoded(), 0));
Log.i("BTLinkProvider/Client", "about to send packet np2");
link.sendPacket(np2, new Device.SendPacketStatusCallback() {
@Override
public void onSuccess() {
@ -409,8 +497,12 @@ public class BluetoothLinkProvider extends BaseLinkProvider {
}
}, true);
} catch (Exception e) {
Log.e("BTLinkProvider/Client", "Connection lost/disconnected on " + device.getAddress(), e);
synchronized (sockets) {
sockets.remove(device, socket);
}
}
}
}

View File

@ -7,6 +7,7 @@
package org.kde.kdeconnect.Backends.BluetoothBackend;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import org.kde.kdeconnect.Helpers.ThreadHelper;
@ -245,7 +246,7 @@ public final class ConnectionMultiplexer implements Closeable {
channel.doClose();
}
channels.clear();
if (socket.isConnected()) {
if (socket != null && socket.isConnected()) {
try {
socket.close();
} catch (IOException ignored) {
@ -438,6 +439,16 @@ public final class ConnectionMultiplexer implements Closeable {
}
}
public String byteArrayToHexString(final byte[] bytes) {
StringBuilder sb = new StringBuilder();
for(byte b : bytes){
sb.append(String.format("0x%02x ", b&0xff));
}
return sb.toString();
}
private void read_message() throws IOException {
byte[] data = new byte[BUFFER_SIZE];
read_buffer(data, 19);
@ -451,6 +462,10 @@ public final class ConnectionMultiplexer implements Closeable {
UUID channel_id = new UUID(channel_id_msb, channel_id_lsb);
if (!receivedProtocolVersion && type != MESSAGE_PROTOCOL_VERSION) {
Log.w("ConnectionMultiplexer", "Received invalid message '" + message+"'");
Log.w("ConnectionMultiplexer", "'data_buffer:("+byteArrayToHexString(data)+") ");
Log.w("ConnectionMultiplexer", "as string: '"+(new String(data, "UTF-8"))+"' ");
throw new IOException("Did not receive protocol version message!");
}
@ -538,11 +553,15 @@ public final class ConnectionMultiplexer implements Closeable {
public void run() {
while (true) {
synchronized (lock) {
if (!open) return;
if (!open) {
Log.w("ConnectionMultiplexer", "connection not open, returning");
return;
}
}
try {
read_message();
} catch (IOException e) {
Log.w("ConnectionMultiplexer", "run caught IOException", e);
handleException(e);
return;
}

View File

@ -494,4 +494,7 @@ public class LanLinkProvider extends BaseLinkProvider {
return "LanLinkProvider";
}
@Override
public int getPriority() { return 20; }
}

View File

@ -40,4 +40,7 @@ public class LoopbackLinkProvider extends BaseLinkProvider {
public String getName() {
return "LoopbackLinkProvider";
}
@Override
public int getPriority() { return 0; }
}

View File

@ -32,6 +32,7 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
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.NotificationHelper;
import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardFloatingActivity;
@ -82,7 +83,7 @@ public class BackgroundService extends Service {
private void registerLinkProviders() {
linkProviders.add(new LanLinkProvider(this));
// linkProviders.add(new LoopbackLinkProvider(this));
// linkProviders.add(new BluetoothLinkProvider(this));
linkProviders.add(new BluetoothLinkProvider(this));
}
public void onNetworkChange(@Nullable Network network) {

View File

@ -37,7 +37,10 @@ import org.kde.kdeconnect_tp.R;
import java.io.IOException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
@ -61,6 +64,7 @@ public class Device implements BaseLink.PacketReceiver {
private MultiValuedMap<String, String> pluginsByIncomingInterface = new ArrayListValuedHashMap<>();
private final SharedPreferences settings;
private final CopyOnWriteArrayList<PluginsChangedListener> pluginsChangedListeners = new CopyOnWriteArrayList<>();
private String connectivityType;
public boolean supportsPacketType(String type) {
if (deviceInfo.incomingCapabilities == null) {
@ -70,6 +74,10 @@ public class Device implements BaseLink.PacketReceiver {
}
}
public String getConnectivityType() {
return connectivityType;
}
public interface PluginsChangedListener {
void onPluginsChanged(@NonNull Device device);
}
@ -84,6 +92,7 @@ public class Device implements BaseLink.PacketReceiver {
this.deviceInfo = DeviceInfo.loadFromSettings(context, deviceId, settings);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.Paired);
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
this.connectivityType = "";
Log.i("Device","Loading trusted device: " + deviceInfo.name);
}
@ -98,6 +107,7 @@ public class Device implements BaseLink.PacketReceiver {
this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE);
this.pairingHandler = new PairingHandler(this, pairingCallback, PairingHandler.PairState.NotPaired);
this.supportedPlugins = new Vector<>(PluginFactory.getAvailablePlugins()); // Assume all are supported until we receive capabilities
this.connectivityType = link.getLinkProvider().getName();
Log.i("Device","Creating untrusted device: "+ deviceInfo.name);
addLink(link);
}
@ -302,7 +312,14 @@ public class Device implements BaseLink.PacketReceiver {
packetQueue = new DevicePacketQueue(this);
}
//FilesHelper.LogOpenFileCount();
links.add(link);
List linksToSort = Arrays.asList(links.toArray());
Collections.sort(linksToSort, (Comparator<BaseLink>) (o1, o2) -> Integer.compare(o2.getLinkProvider().getPriority(), o1.getLinkProvider().getPriority()));
links.clear();
links.addAll(linksToSort);
link.addPacketReceiver(this);
boolean hasChanges = updateDeviceInfo(link.getDeviceInfo());

View File

@ -6,13 +6,17 @@
package org.kde.kdeconnect;
import android.Manifest;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
@ -23,6 +27,7 @@ import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.Plugins.PluginFactory;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.ThemeUtil;
import org.kde.kdeconnect_tp.BuildConfig;
import org.slf4j.impl.HandroidLoggerAdapter;
@ -62,6 +67,7 @@ public class KdeConnect extends Application {
NotificationHelper.initializeChannels(this);
LifecycleHelper.initializeObserver();
loadRememberedDevicesFromSettings();
}
private void setupSL4JLogging() {

View File

@ -39,6 +39,7 @@ public class PairingDeviceItem implements ListAdapter.Item {
final ListItemWithIconEntryBinding binding = ListItemWithIconEntryBinding.inflate(layoutInflater);
binding.listItemEntryIcon.setImageDrawable(device.getIcon());
// binding.listItemEntryTitle.setText(device.getName() + " " + device.getConnectivityType());
binding.listItemEntryTitle.setText(device.getName());
if (device.compareProtocolVersion() != 0) {

View File

@ -39,6 +39,7 @@ import org.kde.kdeconnect.UserInterface.About.AboutFragment
import org.kde.kdeconnect.UserInterface.About.getApplicationAboutData
import org.kde.kdeconnect_tp.R
import org.kde.kdeconnect_tp.databinding.ActivityMainBinding
import java.util.LinkedList
private const val MENU_ENTRY_ADD_DEVICE = 1 //0 means no-selection
private const val MENU_ENTRY_SETTINGS = 2
@ -195,14 +196,20 @@ class MainActivity : AppCompatActivity(), OnSharedPreferenceChangeListener {
}
}
val missingPermissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
if (permissionResult != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), RESULT_NOTIFICATIONS_ENABLED)
missingPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
if(missingPermissions.size > 0){
ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), RESULT_NOTIFICATIONS_ENABLED)
}
}
override fun onDestroy() {

View File

@ -6,6 +6,8 @@
package org.kde.kdeconnect.UserInterface;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
@ -20,6 +22,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
@ -38,6 +41,7 @@ import org.kde.kdeconnect_tp.R;
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String KEY_UDP_BROADCAST_ENABLED = "udp_broadcast_enabled";
public static final String KEY_BLUETOOTH_ENABLED = "bluetooth_enabled";
public static final String KEY_APP_THEME = "theme_pref";
private EditTextPreference renameDevice;
@ -178,6 +182,18 @@ public class SettingsFragment extends PreferenceFragmentCompat {
udpBroadcastDiscovery.setTitle(R.string.enable_udp_broadcast);
screen.addPreference(udpBroadcastDiscovery);
final TwoStatePreference enableBluetoothSupport = new SwitchPreference(context);
enableBluetoothSupport.setDefaultValue(false);
enableBluetoothSupport.setKey(KEY_BLUETOOTH_ENABLED);
enableBluetoothSupport.setTitle("Enable bluetooth (beta)");
enableBluetoothSupport.setOnPreferenceChangeListener((preference, newValue) -> {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && (boolean)newValue) {
ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN}, 2);
}
return true;
});
screen.addPreference(enableBluetoothSupport);
// More settings text
Preference moreSettingsText = new Preference(context);
moreSettingsText.setPersistent(false);