1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-20 20:10:15 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/discovery/GBScanEventProcessor.java

263 lines
9.5 KiB
Java
Raw Normal View History

/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo
2023-08-31 23:23:10 +02:00
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/>. */
2023-08-31 23:23:10 +02:00
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);
2023-08-31 23:23:10 +02:00
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;
2023-08-31 23:23:10 +02:00
if (candidate == null) {
// First time we see this device
LOG.debug("Found {} for the first time", address);
firstTime = true;
2023-08-31 23:23:10 +02:00
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;
}
2023-08-31 23:23:10 +02:00
}
if (processCandidate(candidate)) {
LOG.info(
"Device {} ({}) is supported as '{}' without scanning services",
candidate.getDevice(),
candidate.getName(),
DeviceHelper.getInstance().resolveDeviceType(candidate, false)
);
return true;
2023-08-31 23:23:10 +02:00
}
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();
}
}