1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-19 11:30:44 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/GBScanEventProcessor.java
José Rebelo 8085eb6ec3 Discover bonded devices by default
A lof of devices will simply work anyway even if already paired in
Android bluetooth settings. Discover them by default, but warn the user
if the device is not known to pair correctly if already paired in Android
settings. Allows this warning to be disabled to known working devices.
2024-03-25 20:31:33 +00:00

263 lines
9.5 KiB
Java

/* Copyright (C) 2023-2024 Daniel Dakhno, 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 <https://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.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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");
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 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() {
candidatesByAddress.clear();
eventsToProcessMap.clear();
eventsToProcessQueue.clear();
}
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().resolveDeviceType(candidate, false);
if (deviceType.isSupported() || discoverUnsupported) {
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;
}
LOG.debug("Processing {} events for {}", events.size(), address);
GBDeviceCandidate candidate = candidatesByAddress.get(address);
String previousName = null;
ParcelUuid[] previousUuids = null;
boolean firstTime = false;
if (candidate == null) {
// First time we see this device
LOG.debug("Found {} for the first time", address);
firstTime = true;
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();
}
// 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 (!firstTime) {
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 (processCandidate(candidate)) {
LOG.info(
"Device {} ({}) is supported as '{}' without scanning services",
candidate.getDevice(),
candidate.getName(),
DeviceHelper.getInstance().resolveDeviceType(candidate, false)
);
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();
}
}