mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-29 05:16:51 +01:00
Introduce DiscoveryActivityV2
This commit is contained in:
parent
a85246c279
commit
e078ceff0a
@ -530,6 +530,10 @@
|
||||
android:name=".activities.discovery.DiscoveryActivity"
|
||||
android:label="@string/title_activity_discovery"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.discovery.DiscoveryActivityV2"
|
||||
android:label="@string/title_activity_discovery"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.AndroidPairingActivity"
|
||||
android:label="@string/title_activity_android_pairing" />
|
||||
|
@ -1389,6 +1389,10 @@ public class GBApplication extends Application {
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public static boolean useNewDiscoveryActivity() {
|
||||
return prefs.getBoolean("new_discover_activity", true);
|
||||
}
|
||||
|
||||
public static GBPrefs getGBPrefs() {
|
||||
return gbPrefs;
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
@ -375,7 +376,7 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
GBApplication.deviceService().start();
|
||||
|
||||
if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
launchDiscoveryActivity();
|
||||
} else {
|
||||
GBApplication.deviceService().requestDeviceInfo();
|
||||
}
|
||||
@ -482,7 +483,11 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
}
|
||||
|
||||
private void launchDiscoveryActivity() {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
if (GBApplication.useNewDiscoveryActivity()) {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPairedDevices() {
|
||||
|
@ -401,6 +401,8 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
}
|
||||
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
|
||||
candidate.refreshNameIfUnknown();
|
||||
candidate.addUuids(device.getUuids());
|
||||
DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
|
||||
if (deviceType.isSupported() || discoverUnsupported) {
|
||||
candidate.setDeviceType(deviceType);
|
||||
|
@ -0,0 +1,883 @@
|
||||
/* Copyright (C) 2015-2023 Andreas Shimokawa, boun, Carsten Pfeiffer, Daniel
|
||||
Dakhno, Daniele Gobbetti, JohnnySun, jonnsoft, José Rebelo, Lem Dulfo, Taavi
|
||||
Eomäe, Uwe Hermann
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.discovery;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.util.GB.toast;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.le.BluetoothLeScanner;
|
||||
import android.bluetooth.le.ScanCallback;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.bluetooth.le.ScanRecord;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import android.bluetooth.le.ScanSettings;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DebugActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
|
||||
public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterView.OnItemClickListener,
|
||||
AdapterView.OnItemLongClickListener,
|
||||
BondingInterface,
|
||||
GBScanEventProcessor.Callback {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivityV2.class);
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private static final long SCAN_DURATION = 30000; // 30s
|
||||
private static final long LIST_REFRESH_THRESHOLD_MS = 2500L;
|
||||
private long lastListRefresh = System.currentTimeMillis();
|
||||
|
||||
private final ScanCallback bleScanCallback = new BleScanCallback();
|
||||
|
||||
private ProgressBar bluetoothProgress;
|
||||
private ProgressBar bluetoothLEProgress;
|
||||
|
||||
private DeviceCandidateAdapter deviceCandidateAdapter;
|
||||
private GBDeviceCandidate deviceTarget;
|
||||
private BluetoothAdapter adapter;
|
||||
|
||||
private Button startButton;
|
||||
private boolean scanning;
|
||||
|
||||
private long selectedUnsupportedDeviceKey = DebugActivity.SELECT_DEVICE;
|
||||
|
||||
private final Runnable stopRunnable = () -> {
|
||||
stopDiscovery();
|
||||
LOG.info("Discovery stopped by thread timeout.");
|
||||
};
|
||||
|
||||
private final BroadcastReceiver bluetoothReceiver = new BluetoothReceiver();
|
||||
|
||||
private final GBScanEventProcessor deviceFoundProcessor = new GBScanEventProcessor(this);
|
||||
|
||||
// Array to back the adapter for the UI
|
||||
private final ArrayList<GBDeviceCandidate> deviceCandidates = new ArrayList<>();
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private GBDeviceCandidate getCandidateFromMAC(final BluetoothDevice device) {
|
||||
for (final GBDeviceCandidate candidate : deviceCandidates) {
|
||||
if (candidate.getMacAddress().equals(device.getAddress())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
LOG.warn("This shouldn't happen unless the list somehow emptied itself, device MAC: {}", device.getAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
loadSettings();
|
||||
|
||||
setContentView(R.layout.activity_discovery);
|
||||
|
||||
startButton = findViewById(R.id.discovery_start);
|
||||
startButton.setOnClickListener(v -> toggleDiscovery());
|
||||
|
||||
final Button settingsButton = findViewById(R.id.discovery_preferences);
|
||||
settingsButton.setOnClickListener(v -> {
|
||||
final Intent enableIntent = new Intent(DiscoveryActivityV2.this, DiscoveryPairingPreferenceActivity.class);
|
||||
startActivity(enableIntent);
|
||||
});
|
||||
|
||||
bluetoothProgress = findViewById(R.id.discovery_progressbar);
|
||||
bluetoothProgress.setProgress(0);
|
||||
bluetoothProgress.setIndeterminate(true);
|
||||
bluetoothProgress.setVisibility(View.GONE);
|
||||
|
||||
bluetoothLEProgress = findViewById(R.id.discovery_ble_progressbar);
|
||||
bluetoothLEProgress.setProgress(0);
|
||||
bluetoothLEProgress.setIndeterminate(true);
|
||||
bluetoothLEProgress.setVisibility(View.GONE);
|
||||
|
||||
deviceCandidateAdapter = new DeviceCandidateAdapter(this, deviceCandidates);
|
||||
|
||||
final ListView deviceCandidatesView = findViewById(R.id.discovery_device_candidates_list);
|
||||
deviceCandidatesView.setAdapter(deviceCandidateAdapter);
|
||||
deviceCandidatesView.setOnItemClickListener(this);
|
||||
deviceCandidatesView.setOnItemLongClickListener(this);
|
||||
|
||||
registerBroadcastReceivers();
|
||||
|
||||
checkAndRequestLocationPermission();
|
||||
|
||||
if (!startDiscovery()) {
|
||||
/* if we couldn't start scanning, go back to the main page.
|
||||
A toast will have been shown explaining what's wrong */
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
final List<Parcelable> restoredCandidates = savedInstanceState.getParcelableArrayList("deviceCandidates");
|
||||
if (restoredCandidates != null) {
|
||||
deviceCandidates.clear();
|
||||
for (final Parcelable p : restoredCandidates) {
|
||||
final GBDeviceCandidate candidate = (GBDeviceCandidate) p;
|
||||
deviceCandidates.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterBroadcastReceivers();
|
||||
stopDiscovery();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
unregisterBroadcastReceivers();
|
||||
stopDiscovery();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
unregisterBroadcastReceivers();
|
||||
stopDiscovery();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
loadSettings();
|
||||
registerBroadcastReceivers();
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
private void refreshDeviceList(final boolean throttle) {
|
||||
handler.post(() -> {
|
||||
if (throttle && System.currentTimeMillis() - lastListRefresh < LIST_REFRESH_THRESHOLD_MS) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Refreshing device list");
|
||||
|
||||
// Clear and re-populate the list. deviceFoundProcessor keeps insertion order, so newer devices
|
||||
// will still be at the end
|
||||
deviceCandidates.clear();
|
||||
deviceCandidates.addAll(deviceFoundProcessor.getDevices());
|
||||
|
||||
deviceCandidateAdapter.notifyDataSetChanged();
|
||||
|
||||
lastListRefresh = System.currentTimeMillis();
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleDiscovery() {
|
||||
if (scanning) {
|
||||
stopDiscovery();
|
||||
} else {
|
||||
startDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean startDiscovery() {
|
||||
if (scanning) {
|
||||
LOG.warn("Not starting discovery, because already scanning.");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG.info("Starting discovery");
|
||||
startButton.setText(getString(R.string.discovery_stop_scanning));
|
||||
|
||||
deviceFoundProcessor.clear();
|
||||
deviceFoundProcessor.start();
|
||||
|
||||
refreshDeviceList(false);
|
||||
|
||||
try {
|
||||
if (!ensureBluetoothReady()) {
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.discovery_enable_bluetooth), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GB.supportsBluetoothLE()) {
|
||||
startBTLEDiscovery();
|
||||
}
|
||||
startBTDiscovery();
|
||||
} catch (final SecurityException e) {
|
||||
LOG.error("SecurityException on startDiscovery");
|
||||
deviceFoundProcessor.stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
setScanning(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void stopDiscovery() {
|
||||
LOG.info("Stopping discovery");
|
||||
try {
|
||||
stopBTDiscovery();
|
||||
stopBLEDiscovery();
|
||||
} catch (final SecurityException e) {
|
||||
LOG.error("SecurityException on stopDiscovery");
|
||||
}
|
||||
setScanning(false);
|
||||
deviceFoundProcessor.stop();
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
|
||||
// Refresh the device list one last time when finishing
|
||||
refreshDeviceList(false);
|
||||
}
|
||||
|
||||
public void setScanning(final boolean scanning) {
|
||||
this.scanning = scanning;
|
||||
if (scanning) {
|
||||
startButton.setText(getString(R.string.discovery_stop_scanning));
|
||||
} else {
|
||||
startButton.setText(getString(R.string.discovery_start_scanning));
|
||||
bluetoothProgress.setVisibility(View.GONE);
|
||||
bluetoothLEProgress.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
|
||||
private void startBTLEDiscovery() {
|
||||
LOG.info("Starting BLE discovery");
|
||||
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
|
||||
|
||||
// Filters being non-null would be a very good idea with background scan, but in this case,
|
||||
// not really required.
|
||||
// TODO getScanFilters maybe
|
||||
adapter.getBluetoothLeScanner().startScan(null, getScanSettings(), bleScanCallback);
|
||||
|
||||
LOG.debug("Bluetooth LE discovery started successfully");
|
||||
bluetoothLEProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
|
||||
private void stopBLEDiscovery() {
|
||||
if (adapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BluetoothLeScanner bluetoothLeScanner = adapter.getBluetoothLeScanner();
|
||||
if (bluetoothLeScanner == null) {
|
||||
LOG.warn("Could not get BluetoothLeScanner()!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bleScanCallback == null) {
|
||||
LOG.warn("newLeScanCallback == null!");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
bluetoothLeScanner.stopScan(bleScanCallback);
|
||||
} catch (final NullPointerException e) {
|
||||
LOG.warn("Internal NullPointerException when stopping the scan!");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Stopped BLE discovery");
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a regular Bluetooth scan
|
||||
*/
|
||||
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
|
||||
private void startBTDiscovery() {
|
||||
LOG.info("Starting BT discovery");
|
||||
try {
|
||||
// LineageOS quirk, can't stop scan properly,
|
||||
// if scan has been started by something else
|
||||
stopBTDiscovery();
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
handler.removeMessages(0, stopRunnable);
|
||||
handler.sendMessageDelayed(getPostMessage(stopRunnable), SCAN_DURATION);
|
||||
|
||||
if (adapter.startDiscovery()) {
|
||||
LOG.debug("Discovery started successfully");
|
||||
bluetoothProgress.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
LOG.error("Discovery starting failed");
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
|
||||
private void stopBTDiscovery() {
|
||||
if (adapter == null) return;
|
||||
adapter.cancelDiscovery();
|
||||
LOG.info("Stopped BT discovery");
|
||||
}
|
||||
|
||||
private void bluetoothStateChanged(final int newState) {
|
||||
if (newState == BluetoothAdapter.STATE_ON) {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
startButton.setEnabled(true);
|
||||
} else {
|
||||
this.adapter = null;
|
||||
startButton.setEnabled(false);
|
||||
bluetoothProgress.setVisibility(View.GONE);
|
||||
bluetoothLEProgress.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkBluetoothAvailable() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.warn("No BLUETOOTH_SCAN permission");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.warn("No BLUETOOTH_CONNECT permission");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final BluetoothManager bluetoothService = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
|
||||
if (bluetoothService == null) {
|
||||
LOG.warn("No bluetooth service available");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
final BluetoothAdapter adapter = bluetoothService.getAdapter();
|
||||
if (adapter == null) {
|
||||
LOG.warn("No bluetooth adapter available");
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!adapter.isEnabled()) {
|
||||
LOG.warn("Bluetooth not enabled");
|
||||
final Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
startActivity(enableBtIntent);
|
||||
this.adapter = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.adapter = adapter;
|
||||
return true;
|
||||
}
|
||||
|
||||
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
|
||||
private boolean ensureBluetoothReady() {
|
||||
final boolean available = checkBluetoothAvailable();
|
||||
startButton.setEnabled(available);
|
||||
|
||||
if (available) {
|
||||
adapter.cancelDiscovery();
|
||||
// must not return the result of cancelDiscovery()
|
||||
// appears to return false when currently not scanning
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ScanSettings getScanSettings() {
|
||||
final Prefs prefs = GBApplication.getPrefs();
|
||||
final int level = prefs.getInt("scanning_intensity", 1);
|
||||
|
||||
final ScanSettings.Builder builder = new ScanSettings.Builder()
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY);
|
||||
|
||||
LOG.debug("Device discovery - scanning level: {}", level);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
switch (level) {
|
||||
case 0:
|
||||
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
|
||||
builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
|
||||
break;
|
||||
case 1:
|
||||
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH);
|
||||
builder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
|
||||
break;
|
||||
case 2:
|
||||
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
|
||||
builder.setMatchMode(ScanSettings.MATCH_MODE_STICKY);
|
||||
break;
|
||||
case 3:
|
||||
builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);
|
||||
builder.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown intensity level {}", level);
|
||||
}
|
||||
|
||||
builder.setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private List<ScanFilter> getScanFilters() {
|
||||
final List<ScanFilter> allFilters = new ArrayList<>();
|
||||
for (final DeviceCoordinator coordinator : DeviceHelper.getInstance().getAllCoordinators()) {
|
||||
allFilters.addAll(coordinator.createBLEScanFilters());
|
||||
}
|
||||
return allFilters;
|
||||
}
|
||||
|
||||
private Message getPostMessage(final Runnable runnable) {
|
||||
final Message message = Message.obtain(handler, runnable);
|
||||
message.obj = runnable;
|
||||
return message;
|
||||
}
|
||||
|
||||
private void checkAndRequestLocationPermission() {
|
||||
/* This is more or less a copy of what's in ControlCenterv2, but
|
||||
we do this in case the permissions weren't requested since there
|
||||
is no way we can scan without this stuff */
|
||||
List<String> wantedPermissions = new ArrayList<>();
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access coarse location!");
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access fine location!");
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
// if we need location permissions, request both together to avoid a bunch of dialogs
|
||||
if (wantedPermissions.size() > 0) {
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
|
||||
wantedPermissions.clear();
|
||||
}
|
||||
// Now we have to request background location separately!
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access background location!");
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
|
||||
}
|
||||
}
|
||||
// Now, we can request Bluetooth permissions....
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access Bluetooth scanning!");
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.error_no_bluetooth_scan), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access Bluetooth connection!");
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.error_no_bluetooth_connect), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
|
||||
}
|
||||
}
|
||||
if (wantedPermissions.size() > 0) {
|
||||
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
|
||||
} else {
|
||||
ActivityResultLauncher<String[]> requestMultiplePermissionsLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {
|
||||
if (!isGranted.containsValue(false)) {
|
||||
// Permission is granted. Continue the action or workflow in your app.
|
||||
// should we do startDiscovery here??
|
||||
} else {
|
||||
// Explain to the user that the feature is unavailable because the feature requires a permission that the user has denied.
|
||||
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
});
|
||||
requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
LocationManager locationManager = (LocationManager) DiscoveryActivityV2.this.getSystemService(Context.LOCATION_SERVICE);
|
||||
try {
|
||||
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
|
||||
// Do nothing
|
||||
LOG.debug("Some location provider is enabled, assuming location is enabled");
|
||||
} else {
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.require_location_provider), Toast.LENGTH_LONG, GB.ERROR);
|
||||
DiscoveryActivityV2.this.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
|
||||
// We can't be sure location was enabled, cancel scan start and wait for new user action
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.error_location_enabled_mandatory), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
} catch (final Exception ex) {
|
||||
LOG.error("Exception when checking location status", ex);
|
||||
}
|
||||
LOG.info("Permissions seems to be fine for scanning");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(final AdapterView<?> parent, final View view, final int position, final long id) {
|
||||
final GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
|
||||
if (deviceCandidate == null) {
|
||||
LOG.error("Device candidate clicked, but item not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceCandidate.getDeviceType().isSupported()) {
|
||||
LOG.warn("Unsupported device candidate {}", deviceCandidate);
|
||||
copyDetailsToClipboard(deviceCandidate);
|
||||
return;
|
||||
}
|
||||
|
||||
stopDiscovery();
|
||||
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
|
||||
LOG.info("Using device candidate {} with coordinator {}", deviceCandidate, coordinator.getClass());
|
||||
|
||||
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
|
||||
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress());
|
||||
|
||||
final String authKey = sharedPrefs.getString("authkey", null);
|
||||
if (authKey == null || authKey.isEmpty()) {
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.discovery_need_to_enter_authkey), Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
} else if (authKey.getBytes().length < 34 || !authKey.startsWith("0x")) {
|
||||
toast(DiscoveryActivityV2.this, getString(R.string.discovery_entered_invalid_authkey), Toast.LENGTH_LONG, GB.WARN);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
|
||||
if (pairingActivity != null) {
|
||||
final Intent intent = new Intent(this, pairingActivity);
|
||||
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE ||
|
||||
coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_LAZY) {
|
||||
LOG.info("No bonding needed, according to coordinator, so connecting right away");
|
||||
BondingUtil.connectThenComplete(this, deviceCandidate);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deviceTarget = deviceCandidate;
|
||||
BondingUtil.initiateCorrectBonding(this, deviceCandidate);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error pairing device {}", deviceCandidate.getMacAddress(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDetailsToClipboard(final GBDeviceCandidate deviceCandidate) {
|
||||
final List<String> deviceDetails = new ArrayList<>();
|
||||
deviceDetails.add(deviceCandidate.getName());
|
||||
deviceDetails.add(deviceCandidate.getMacAddress());
|
||||
try {
|
||||
for (final ParcelUuid uuid : deviceCandidate.getServiceUuids()) {
|
||||
deviceDetails.add(uuid.getUuid().toString());
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Error collecting device uuids", e);
|
||||
}
|
||||
final String clipboardData = TextUtils.join(", ", deviceDetails);
|
||||
final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
final ClipData clip = ClipData.newPlainText(deviceCandidate.getName(), clipboardData);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
toast(this, "Device details copied to clipboard", Toast.LENGTH_SHORT, GB.INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(final AdapterView<?> adapterView, final View view, final int position, final long id) {
|
||||
stopDiscovery();
|
||||
|
||||
final GBDeviceCandidate deviceCandidate = deviceCandidates.get(position);
|
||||
if (deviceCandidate == null) {
|
||||
LOG.error("Device candidate clicked, but item not found");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!deviceCandidate.getDeviceType().isSupported()) {
|
||||
showUnsupportedDeviceDialog(deviceCandidate);
|
||||
return true;
|
||||
}
|
||||
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
|
||||
final GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
|
||||
if (coordinator.getSupportedDeviceSpecificSettings(device) == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Intent startIntent;
|
||||
startIntent = new Intent(this, DeviceSettingsActivity.class);
|
||||
startIntent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
|
||||
startIntent.putExtra(DeviceSettingsActivity.MENU_ENTRY_POINT, DeviceSettingsActivity.MENU_ENTRY_POINTS.AUTH_SETTINGS);
|
||||
} else {
|
||||
startIntent.putExtra(DeviceSettingsActivity.MENU_ENTRY_POINT, DeviceSettingsActivity.MENU_ENTRY_POINTS.DEVICE_SETTINGS);
|
||||
}
|
||||
startActivity(startIntent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showUnsupportedDeviceDialog(final GBDeviceCandidate deviceCandidate) {
|
||||
LOG.info("Unsupported device candidate selected: {}", deviceCandidate);
|
||||
|
||||
final Map<String, Pair<Long, Integer>> allDevices = DebugActivity.getAllSupportedDevices(getApplicationContext());
|
||||
|
||||
final LinearLayout linearLayout = new LinearLayout(DiscoveryActivityV2.this);
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final ArrayList<SpinnerWithIconItem> deviceListArray = new ArrayList<>();
|
||||
for (Map.Entry<String, Pair<Long, Integer>> item : allDevices.entrySet()) {
|
||||
deviceListArray.add(new SpinnerWithIconItem(item.getKey(), item.getValue().first, item.getValue().second));
|
||||
}
|
||||
final SpinnerWithIconAdapter deviceListAdapter = new SpinnerWithIconAdapter(
|
||||
DiscoveryActivityV2.this,
|
||||
R.layout.spinner_with_image_layout,
|
||||
R.id.spinner_item_text,
|
||||
deviceListArray
|
||||
);
|
||||
|
||||
final Spinner deviceListSpinner = new Spinner(DiscoveryActivityV2.this);
|
||||
deviceListSpinner.setAdapter(deviceListAdapter);
|
||||
deviceListSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(final AdapterView<?> parent, final View view, final int pos, final long id) {
|
||||
final SpinnerWithIconItem selectedItem = (SpinnerWithIconItem) parent.getItemAtPosition(pos);
|
||||
selectedUnsupportedDeviceKey = selectedItem.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(final AdapterView<?> arg0) {
|
||||
}
|
||||
});
|
||||
linearLayout.addView(deviceListSpinner);
|
||||
|
||||
final LinearLayout macLayout = new LinearLayout(DiscoveryActivityV2.this);
|
||||
macLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
macLayout.setPadding(20, 0, 20, 0);
|
||||
linearLayout.addView(macLayout);
|
||||
|
||||
new MaterialAlertDialogBuilder(DiscoveryActivityV2.this)
|
||||
.setCancelable(true)
|
||||
.setTitle(R.string.add_test_device)
|
||||
.setView(linearLayout)
|
||||
.setPositiveButton(R.string.ok, (dialog, which) -> {
|
||||
if (selectedUnsupportedDeviceKey != DebugActivity.SELECT_DEVICE) {
|
||||
DebugActivity.createTestDevice(DiscoveryActivityV2.this, selectedUnsupportedDeviceKey, deviceCandidate.getMacAddress());
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.Cancel, (dialog, which) -> {
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBondingComplete(final boolean success) {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceCandidate getCurrentTarget() {
|
||||
return this.deviceTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBroadcastReceivers() {
|
||||
final IntentFilter bluetoothIntents = new IntentFilter();
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_FOUND);
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_UUID);
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
bluetoothIntents.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
|
||||
bluetoothIntents.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
|
||||
|
||||
registerReceiver(bluetoothReceiver, bluetoothIntents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterBroadcastReceivers() {
|
||||
AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
final Prefs prefs = GBApplication.getPrefs();
|
||||
deviceFoundProcessor.setIgnoreBonded(prefs.getBoolean("ignore_bonded_devices", true));
|
||||
deviceFoundProcessor.setDiscoverUnsupported(prefs.getBoolean("discover_unsupported_devices", false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceChanged() {
|
||||
refreshDeviceList(true);
|
||||
}
|
||||
|
||||
private final class BluetoothReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
switch (Objects.requireNonNull(intent.getAction())) {
|
||||
case BluetoothAdapter.ACTION_DISCOVERY_STARTED: {
|
||||
LOG.debug("ACTION_DISCOVERY_STARTED");
|
||||
break;
|
||||
}
|
||||
case BluetoothAdapter.ACTION_STATE_CHANGED: {
|
||||
LOG.debug("ACTION_STATE_CHANGED ");
|
||||
bluetoothStateChanged(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF));
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.ACTION_FOUND: {
|
||||
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
LOG.warn("ACTION_FOUND with null device");
|
||||
return;
|
||||
}
|
||||
LOG.debug("ACTION_FOUND {}", device.getAddress());
|
||||
final short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
|
||||
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, null));
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.ACTION_UUID: {
|
||||
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
LOG.warn("ACTION_UUID with null device");
|
||||
return;
|
||||
}
|
||||
LOG.debug("ACTION_UUID {}", device.getAddress());
|
||||
final short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, GBDevice.RSSI_UNKNOWN);
|
||||
final Parcelable[] uuids = intent.getParcelableArrayExtra(BluetoothDevice.EXTRA_UUID);
|
||||
final ParcelUuid[] uuids2 = AndroidUtils.toParcelUuids(uuids);
|
||||
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, uuids2));
|
||||
break;
|
||||
}
|
||||
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: {
|
||||
LOG.debug("ACTION_BOND_STATE_CHANGED");
|
||||
final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
if (device != null) {
|
||||
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
|
||||
LOG.debug("Bond state: {}", bondState);
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
BondingUtil.handleDeviceBonded((BondingInterface) context, getCandidateFromMAC(device));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class BleScanCallback extends ScanCallback {
|
||||
@Override
|
||||
public void onScanResult(final int callbackType, final ScanResult result) {
|
||||
super.onScanResult(callbackType, result);
|
||||
try {
|
||||
final ScanRecord scanRecord = result.getScanRecord();
|
||||
ParcelUuid[] uuids = null;
|
||||
if (scanRecord != null) {
|
||||
final List<ParcelUuid> serviceUuids = scanRecord.getServiceUuids();
|
||||
if (serviceUuids != null) {
|
||||
uuids = serviceUuids.toArray(new ParcelUuid[0]);
|
||||
}
|
||||
}
|
||||
final BluetoothDevice device = result.getDevice();
|
||||
final short rssi = (short) result.getRssi();
|
||||
LOG.debug("BLE result: {}, {}, {}", device.getAddress(), ((scanRecord != null) ? scanRecord.getBytes().length : -1), rssi);
|
||||
deviceFoundProcessor.scheduleProcessing(new GBScanEvent(device, rssi, uuids));
|
||||
} catch (final Exception e) {
|
||||
LOG.warn("Error handling BLE scan result", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* Copyright (C) 2023 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.discovery;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A scan event originating from either BT or BLE scan. References the BluetoothDevice, rssi,
|
||||
* and service UUIDs, if any.
|
||||
*/
|
||||
public class GBScanEvent {
|
||||
private final BluetoothDevice device;
|
||||
private final short rssi;
|
||||
|
||||
@Nullable
|
||||
private final ParcelUuid[] serviceUuids;
|
||||
|
||||
public GBScanEvent(final BluetoothDevice device, final short rssi, @Nullable final ParcelUuid[] serviceUuids) {
|
||||
this.device = device;
|
||||
this.rssi = rssi;
|
||||
this.serviceUuids = serviceUuids;
|
||||
}
|
||||
|
||||
public BluetoothDevice getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public short getRssi() {
|
||||
return rssi;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ParcelUuid[] getServiceUuids() {
|
||||
return serviceUuids;
|
||||
}
|
||||
}
|
@ -0,0 +1,284 @@
|
||||
/* Copyright (C) 2015-2023 Andreas Shimokawa, boun, Carsten Pfeiffer, Daniel
|
||||
Dakhno, Daniele Gobbetti, JohnnySun, jonnsoft, José Rebelo, Lem Dulfo, Taavi
|
||||
Eomäe, Uwe Hermann
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.discovery;
|
||||
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
|
||||
/**
|
||||
* A dedicated thread to process {@link GBScanEvent}s. This class keeps a map from mac address to
|
||||
* GBDeviceCandidate, with the current known state of each candidate.
|
||||
* <p>
|
||||
* Processing works as follows:
|
||||
* - The processor consumes mac addresses from the eventsToProcessQueue
|
||||
* - Mac addresses are placed on the queue when there are one or more new GBScanEvents to process in
|
||||
* the eventsToProcessMap map
|
||||
* - The eventsToProcessMap contains a list of events per device, so that they can be processed in batch
|
||||
* - The GBDeviceEvent for the corresponding mac address in candidatesByAddress gets updated with the new
|
||||
* information, and matched against the coordinators.
|
||||
*/
|
||||
public final class GBScanEventProcessor implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GBScanEventProcessor.class);
|
||||
|
||||
private static final ParcelUuid ZERO_UUID = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000");
|
||||
|
||||
// Devices that can be ignored by just the address (eg. already bonded)
|
||||
private final Set<String> devicesToIgnore = new HashSet<>();
|
||||
private final Map<String, GBDeviceCandidate> candidatesByAddress = new LinkedHashMap<>();
|
||||
|
||||
private final BlockingQueue<String> eventsToProcessQueue = new LinkedBlockingQueue<>();
|
||||
private final Map<String, List<GBScanEvent>> eventsToProcessMap = new HashMap<>();
|
||||
|
||||
private boolean ignoreBonded = true;
|
||||
private boolean discoverUnsupported = false;
|
||||
|
||||
private volatile boolean running = false;
|
||||
private Thread thread = null;
|
||||
|
||||
private final Callback callback;
|
||||
|
||||
public GBScanEventProcessor(final Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Device Found Processor Thread started.");
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
LOG.debug("Polling found devices queue, current size = {}", eventsToProcessQueue.size());
|
||||
final String candidateAddress = eventsToProcessQueue.take();
|
||||
if (candidateAddress != null) {
|
||||
if (processAllScanEvents(candidateAddress)) {
|
||||
callback.onDeviceChanged();
|
||||
}
|
||||
}
|
||||
} catch (final InterruptedException e) {
|
||||
LOG.warn("Processing thread interrupted");
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (running) {
|
||||
LOG.warn("Already running!");
|
||||
return;
|
||||
}
|
||||
|
||||
running = true;
|
||||
thread = new Thread("Gadgetbridge Device Found Processor Thread") {
|
||||
@Override
|
||||
public void run() {
|
||||
GBScanEventProcessor.this.run();
|
||||
}
|
||||
};
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
devicesToIgnore.clear();
|
||||
candidatesByAddress.clear();
|
||||
eventsToProcessMap.clear();
|
||||
eventsToProcessQueue.clear();
|
||||
}
|
||||
|
||||
public void setIgnoreBonded(boolean ignoreBonded) {
|
||||
this.ignoreBonded = ignoreBonded;
|
||||
}
|
||||
|
||||
public void setDiscoverUnsupported(boolean discoverUnsupported) {
|
||||
this.discoverUnsupported = discoverUnsupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current list of GBDeviceCandidates. The candidates are cloned, since they can be
|
||||
* modified concurrently by the processor.
|
||||
*/
|
||||
public List<GBDeviceCandidate> getDevices() {
|
||||
final List<GBDeviceCandidate> ret = new ArrayList<>();
|
||||
// candidatesByAddress keeps insertion order, so newer devices will be at the end
|
||||
synchronized (candidatesByAddress) {
|
||||
for (final Map.Entry<String, GBDeviceCandidate> entry : candidatesByAddress.entrySet()) {
|
||||
ret.add(entry.getValue().clone());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a {@link GBScanEvent} to be processed asynchronously.
|
||||
*/
|
||||
public void scheduleProcessing(final GBScanEvent event) {
|
||||
LOG.debug("Scheduling {} for processing ({})", event.getDevice().getAddress(), event.getServiceUuids());
|
||||
|
||||
final String address = event.getDevice().getAddress();
|
||||
synchronized (eventsToProcessMap) {
|
||||
if (!eventsToProcessMap.containsKey(address)) {
|
||||
eventsToProcessMap.put(address, new LinkedList<>());
|
||||
}
|
||||
Objects.requireNonNull(eventsToProcessMap.get(address)).add(event);
|
||||
}
|
||||
|
||||
try {
|
||||
eventsToProcessQueue.put(address);
|
||||
} catch (final InterruptedException e) {
|
||||
LOG.error("Failed to put device on processing queue", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processCandidate(final GBDeviceCandidate candidate) {
|
||||
LOG.debug("found device: {}, {}", candidate.getName(), candidate.getMacAddress());
|
||||
if (LOG.isDebugEnabled()) {
|
||||
final ParcelUuid[] uuids = candidate.getServiceUuids();
|
||||
if (uuids != null && uuids.length > 0) {
|
||||
for (ParcelUuid uuid : uuids) {
|
||||
LOG.debug(" supports uuid: " + uuid.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final DeviceType deviceType = DeviceHelper.getInstance().getSupportedType(candidate);
|
||||
|
||||
if (deviceType.isSupported() || discoverUnsupported) {
|
||||
candidate.setDeviceType(deviceType);
|
||||
synchronized (candidatesByAddress) {
|
||||
candidatesByAddress.put(candidate.getMacAddress(), candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return deviceType.isSupported();
|
||||
}
|
||||
|
||||
private boolean processAllScanEvents(final String address) {
|
||||
final List<GBScanEvent> events;
|
||||
synchronized (eventsToProcessMap) {
|
||||
events = eventsToProcessMap.remove(address);
|
||||
}
|
||||
if (events == null || events.isEmpty()) {
|
||||
LOG.warn("Attempted to process {}, but found no events", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (devicesToIgnore.contains(address)) {
|
||||
LOG.trace("Ignoring {} events for {}", events.size(), address);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG.debug("Processing {} events for {}", events.size(), address);
|
||||
|
||||
GBDeviceCandidate candidate = candidatesByAddress.get(address);
|
||||
|
||||
String previousName = null;
|
||||
ParcelUuid[] previousUuids = null;
|
||||
|
||||
if (candidate == null) {
|
||||
// First time we see this device
|
||||
LOG.debug("Found {} for the first time", address);
|
||||
final GBScanEvent firstEvent = events.get(0);
|
||||
events.remove(0);
|
||||
candidate = new GBDeviceCandidate(firstEvent.getDevice(), firstEvent.getRssi(), firstEvent.getServiceUuids());
|
||||
} else {
|
||||
previousName = candidate.getName();
|
||||
previousUuids = candidate.getServiceUuids();
|
||||
}
|
||||
|
||||
if (candidate.isBonded() && ignoreBonded) {
|
||||
LOG.trace("Ignoring already bonded device {}", address);
|
||||
devicesToIgnore.add(address);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the device with the remaining events
|
||||
for (final GBScanEvent event : events) {
|
||||
candidate.setRssi(event.getRssi());
|
||||
candidate.addUuids(event.getServiceUuids());
|
||||
}
|
||||
|
||||
candidate.refreshNameIfUnknown();
|
||||
try {
|
||||
candidate.addUuids(candidate.getDevice().getUuids());
|
||||
} catch (final SecurityException e) {
|
||||
LOG.error("SecurityException on candidate.getDevice().getUuids()");
|
||||
}
|
||||
|
||||
if (Objects.equals(candidate.getName(), previousName) && Arrays.equals(candidate.getServiceUuids(), previousUuids)) {
|
||||
// Neither name nor uuids changed, do not reprocess
|
||||
LOG.trace("Not reprocessing {} due to no changes", address);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate.isNameKnown()) {
|
||||
if (processCandidate(candidate)) {
|
||||
LOG.info(
|
||||
"Device {} ({}) is supported as '{}' without scanning services",
|
||||
candidate.getDevice(),
|
||||
candidate.getName(),
|
||||
candidate.getDeviceType()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate.getServiceUuids().length == 0 || (candidate.getServiceUuids().length == 1 && candidate.getServiceUuids()[0].equals(ZERO_UUID))) {
|
||||
LOG.debug("Fetching uuids for {} with sdp", candidate.getDevice().getAddress());
|
||||
try {
|
||||
candidate.getDevice().fetchUuidsWithSdp();
|
||||
} catch (final SecurityException e) {
|
||||
LOG.error("SecurityException on candidate.getDevice().fetchUuidsWithSdp()");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onDeviceChanged();
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ public class DeviceCandidateAdapter extends ArrayAdapter<GBDeviceCandidate> {
|
||||
deviceImageView.setImageResource(device.getDeviceType().getIcon());
|
||||
|
||||
final List<String> statusLines = new ArrayList<>();
|
||||
if (device.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) {
|
||||
if (device.isBonded()) {
|
||||
statusLines.add(getContext().getString(R.string.device_is_currently_bonded));
|
||||
if (!GBApplication.getPrefs().getBoolean("ignore_bonded_devices", true)) { // This could be passed to the constructor instead
|
||||
deviceImageView.setImageResource(device.getDeviceType().getDisabledIcon());
|
||||
|
@ -33,10 +33,12 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -65,7 +67,11 @@ public class LenovoWatchPairingActivity extends AbstractGBActivity implements Bo
|
||||
}
|
||||
if (deviceCandidate == null) {
|
||||
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
if (GBApplication.useNewDiscoveryActivity()) {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AboutUserPreferencesActiv
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -76,7 +77,11 @@ public class MiBandPairingActivity extends AbstractGBActivity implements Bonding
|
||||
|
||||
if (deviceCandidate == null) {
|
||||
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
if (GBApplication.useNewDiscoveryActivity()) {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@ -172,7 +173,11 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding
|
||||
if (success) {
|
||||
startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
if (GBApplication.useNewDiscoveryActivity()) {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not a LE Pebble, initiate a connection when bonding is complete
|
||||
|
@ -31,10 +31,12 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -63,7 +65,11 @@ public class Watch9PairingActivity extends AbstractGBActivity implements Bonding
|
||||
|
||||
if (deviceCandidate == null) {
|
||||
Toast.makeText(this, getString(R.string.message_cannot_pair_no_mac), Toast.LENGTH_SHORT).show();
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
if (GBApplication.useNewDiscoveryActivity()) {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
} else {
|
||||
startActivity(new Intent(this, DiscoveryActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP));
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@ -43,19 +43,22 @@ import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
* Gadgetbridge. Only if a DeviceCoordinator steps up and confirms to
|
||||
* support this candidate, will the candidate be promoted to a GBDevice.
|
||||
*/
|
||||
public class GBDeviceCandidate implements Parcelable {
|
||||
public class GBDeviceCandidate implements Parcelable, Cloneable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GBDeviceCandidate.class);
|
||||
|
||||
private final BluetoothDevice device;
|
||||
private final short rssi;
|
||||
private final ParcelUuid[] serviceUuids;
|
||||
private BluetoothDevice device;
|
||||
private short rssi;
|
||||
private ParcelUuid[] serviceUuids;
|
||||
private DeviceType deviceType = DeviceType.UNKNOWN;
|
||||
|
||||
// Cached values for device name and bond status, to avoid querying the remote bt device
|
||||
private String deviceName;
|
||||
private Boolean isBonded = null;
|
||||
|
||||
public GBDeviceCandidate(BluetoothDevice device, short rssi, ParcelUuid[] serviceUuids) {
|
||||
this.device = device;
|
||||
this.rssi = rssi;
|
||||
this.serviceUuids = mergeServiceUuids(serviceUuids, device.getUuids());
|
||||
this.serviceUuids = serviceUuids;
|
||||
}
|
||||
|
||||
private GBDeviceCandidate(Parcel in) {
|
||||
@ -66,8 +69,13 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
rssi = (short) in.readInt();
|
||||
deviceType = DeviceType.valueOf(in.readString());
|
||||
|
||||
ParcelUuid[] uuids = AndroidUtils.toParcelUuids(in.readParcelableArray(getClass().getClassLoader()));
|
||||
serviceUuids = mergeServiceUuids(uuids, device.getUuids());
|
||||
serviceUuids = AndroidUtils.toParcelUuids(in.readParcelableArray(getClass().getClassLoader()));
|
||||
|
||||
deviceName = in.readString();
|
||||
final int isBondedInt = in.readInt();
|
||||
if (isBondedInt != -1) {
|
||||
isBonded = (isBondedInt == 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -76,6 +84,12 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
dest.writeInt(rssi);
|
||||
dest.writeString(deviceType.name());
|
||||
dest.writeParcelableArray(serviceUuids, 0);
|
||||
dest.writeString(deviceName);
|
||||
if (isBonded == null) {
|
||||
dest.writeInt(-1);
|
||||
} else {
|
||||
dest.writeInt(isBonded ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Creator<GBDeviceCandidate> CREATOR = new Creator<GBDeviceCandidate>() {
|
||||
@ -107,7 +121,7 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
}
|
||||
|
||||
private ParcelUuid[] mergeServiceUuids(ParcelUuid[] serviceUuids, ParcelUuid[] deviceUuids) {
|
||||
Set<ParcelUuid> uuids = new HashSet<>();
|
||||
Set<ParcelUuid> uuids = new LinkedHashSet<>();
|
||||
if (serviceUuids != null) {
|
||||
uuids.addAll(Arrays.asList(serviceUuids));
|
||||
}
|
||||
@ -117,6 +131,30 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
return uuids.toArray(new ParcelUuid[0]);
|
||||
}
|
||||
|
||||
public void addUuids(ParcelUuid[] newUuids) {
|
||||
this.serviceUuids = mergeServiceUuids(serviceUuids, newUuids);
|
||||
}
|
||||
|
||||
public void setRssi(short rssi) {
|
||||
this.rssi = rssi;
|
||||
}
|
||||
|
||||
public boolean isBonded() {
|
||||
if (isBonded == null) {
|
||||
try {
|
||||
isBonded = device.getBondState() == BluetoothDevice.BOND_BONDED;
|
||||
} catch (final SecurityException e) {
|
||||
/* This should never happen because we need all the permissions
|
||||
to get to the point where we can even scan, but 'SecurityException' check
|
||||
is added to stop Android Studio errors */
|
||||
LOG.error("SecurityException on getBonded");
|
||||
isBonded = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isBonded;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ParcelUuid[] getServiceUuids() {
|
||||
return serviceUuids;
|
||||
@ -138,24 +176,37 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (this.deviceName != null ) {
|
||||
return this.deviceName;
|
||||
if (isNameKnown()) {
|
||||
return deviceName;
|
||||
}
|
||||
return "(unknown)";
|
||||
}
|
||||
|
||||
public void refreshNameIfUnknown() {
|
||||
if (isNameKnown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = device.getClass().getMethod("getAliasName");
|
||||
if (method != null) {
|
||||
deviceName = (String) method.invoke(device);
|
||||
final Method method = device.getClass().getMethod("getAliasName");
|
||||
deviceName = (String) method.invoke(device);
|
||||
} catch (final NoSuchMethodException ignore) {
|
||||
// ignored
|
||||
} catch (final IllegalAccessException | InvocationTargetException ignore) {
|
||||
LOG.warn("Could not get device alias for {}", device.getAddress());
|
||||
}
|
||||
if (deviceName == null || deviceName.isEmpty()) {
|
||||
try {
|
||||
deviceName = device.getName();
|
||||
} catch (final SecurityException e) {
|
||||
// Should never happen
|
||||
LOG.error("SecurityException on device.getName");
|
||||
}
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ignore) {
|
||||
LOG.info("Could not get device alias for " + device.getName());
|
||||
}
|
||||
if (deviceName == null || deviceName.length() == 0) {
|
||||
deviceName = device.getName();
|
||||
}
|
||||
if (deviceName == null || deviceName.length() == 0) {
|
||||
deviceName = "(unknown)";
|
||||
}
|
||||
return deviceName;
|
||||
}
|
||||
|
||||
public boolean isNameKnown() {
|
||||
return deviceName != null && !deviceName.isEmpty();
|
||||
}
|
||||
|
||||
public short getRssi() {
|
||||
@ -189,4 +240,21 @@ public class GBDeviceCandidate implements Parcelable {
|
||||
public String toString() {
|
||||
return getName() + ": " + getMacAddress() + " (" + getDeviceType() + ")";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public GBDeviceCandidate clone() {
|
||||
try {
|
||||
final GBDeviceCandidate clone = (GBDeviceCandidate) super.clone();
|
||||
clone.device = this.device;
|
||||
clone.rssi = this.rssi;
|
||||
clone.serviceUuids = this.serviceUuids;
|
||||
clone.deviceType = this.deviceType;
|
||||
clone.deviceName = this.deviceName;
|
||||
clone.isBonded = this.isBonded;
|
||||
return clone;
|
||||
} catch (final CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -242,6 +242,7 @@ public class DeviceHelper {
|
||||
|
||||
public GBDevice toSupportedDevice(BluetoothDevice device) {
|
||||
GBDeviceCandidate candidate = new GBDeviceCandidate(device, GBDevice.RSSI_UNKNOWN, device.getUuids());
|
||||
candidate.refreshNameIfUnknown();
|
||||
return toSupportedDevice(candidate);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivity">
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
|
@ -1614,6 +1614,8 @@
|
||||
<string name="ignore_bonded_devices_description">Enabling this option will ignore devices that have been bonded/paired already when scanning</string>
|
||||
<string name="discover_unsupported_devices">Discover unsupported devices</string>
|
||||
<string name="discover_unsupported_devices_description">Enabling this option will display all discovered bluetooth devices when scanning. Short tap will copy device name and mac address to clipboard. Long press will launch `Add test device` dialog. Can cause potential app freezing issues.</string>
|
||||
<string name="new_discover_activity_title">Enable new discover activity</string>
|
||||
<string name="new_discover_activity_description">Enable the new discover activity, which should fix device discovery issues. Disable this if you face any issues while searching for or pairing with your device.</string>
|
||||
<string name="error_location_enabled_mandatory">Location must be turned on to scan for devices</string>
|
||||
<string name="sonyswr12_settings_title">Sony SWR12 Settings</string>
|
||||
<string name="sonyswr12_settings_low_vibration">Low vibration enabled</string>
|
||||
|
@ -23,6 +23,13 @@
|
||||
android:summary="@string/discover_unsupported_devices_description"
|
||||
android:title="@string/discover_unsupported_devices"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="new_discover_activity"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/new_discover_activity_description"
|
||||
android:title="@string/new_discover_activity_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
<SeekBarPreference
|
||||
android:defaultValue="1"
|
||||
android:key="scanning_intensity"
|
||||
|
Loading…
Reference in New Issue
Block a user