blob: 6e9ab5901b79e864c0e1a6680554149d4d85953d [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.impl.google.discovery.plugins.ble;
import com.google.common.base.Equivalence;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import org.joda.time.Duration;
import org.joda.time.Instant;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import io.v.x.ref.lib.discovery.AdInfo;
import io.v.impl.google.lib.discovery.Plugin;
/**
* A cache of ble devices that were seen recently.
* <p>
* The current Vanadium BLE protocol requires connecting to the advertiser
* to grab the attributes and the addrs. This can be expensive so we only
* refetch the data if its stamp changed.
*/
class DeviceCache {
// Stores an interface name and a {@link Plugin.ScanHandler}.
private static class Scanner {
private final String interfaceName;
private final Plugin.ScanHandler handler;
private Scanner(String interfaceName, Plugin.ScanHandler handler) {
this.interfaceName = interfaceName;
this.handler = handler;
}
}
// Stores advertisements from a device with a stamp.
private static class CacheEntry {
private final long stamp;
private String deviceId;
private Set<Equivalence.Wrapper<AdInfo>> adInfos;
private Instant lastSeen;
private CacheEntry(long stamp, String deviceId, Set<Equivalence.Wrapper<AdInfo>> adInfos) {
this.stamp = stamp;
this.deviceId = deviceId;
this.adInfos = adInfos;
this.lastSeen = new Instant();
}
}
private static final Equivalence<AdInfo> ADINFO_EQUIVALENCE =
new Equivalence<AdInfo>() {
@Override
protected boolean doEquivalent(AdInfo a, AdInfo b) {
return a.getAd().getId().equals(b.getAd().getId())
&& a.getHash().equals(b.getHash());
}
@Override
protected int doHash(AdInfo adinfo) {
return Objects.hashCode(adinfo.getAd().getId(), adinfo.getHash());
}
};
private static final Function<AdInfo, Equivalence.Wrapper<AdInfo>> ADINFO_WRAPPER =
new Function<AdInfo, Equivalence.Wrapper<AdInfo>>() {
@Override
public Equivalence.Wrapper<AdInfo> apply(AdInfo adinfo) {
return ADINFO_EQUIVALENCE.wrap(adinfo);
}
};
private final Map<Long, CacheEntry> cacheByStamp = new HashMap<>();
private final Map<String, CacheEntry> cacheByDeviceId = new HashMap<>();
private final SetMultimap<String, Equivalence.Wrapper<AdInfo>> adInfosByInterfaceName =
HashMultimap.create();
private final SetMultimap<String, Scanner> scannersByInterfaceName = HashMultimap.create();
private final Map<Plugin.ScanHandler, Scanner> scannersByHandler = new HashMap<>();
private final Duration maxAge;
private ScheduledExecutorService timer;
DeviceCache(Duration maxAge) {
this.maxAge = maxAge;
this.timer = Executors.newSingleThreadScheduledExecutor();
long periodicity = maxAge.getMillis() / 2;
timer.scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
removeStaleEntries();
}
},
periodicity,
periodicity,
TimeUnit.MILLISECONDS);
}
private void removeStaleEntries() {
synchronized (this) {
Iterator<Map.Entry<Long, CacheEntry>> it = cacheByStamp.entrySet().iterator();
while (it.hasNext()) {
CacheEntry entry = it.next().getValue();
if (entry.lastSeen.plus(maxAge).isBeforeNow()) {
it.remove();
cacheByDeviceId.remove(entry.deviceId);
for (Equivalence.Wrapper<AdInfo> wrapper : entry.adInfos) {
AdInfo adinfo = wrapper.get();
adInfosByInterfaceName.remove(adinfo.getAd().getInterfaceName(), wrapper);
adinfo.setLost(true);
handleUpdate(adinfo);
}
}
}
}
}
/**
* Cleans up the cache's state and shutdowns the eviction thread.
*/
void shutdownCache() {
timer.shutdown();
}
/**
* Returns whether this stamp has been seen before.
*
* @param stamp the stamp of the advertisement
* @param deviceId the deviceId of the advertisement (used to handle rotating ids)
* @return true iff this stamp is in the cache
*/
boolean haveSeenStamp(long stamp, String deviceId) {
synchronized (this) {
CacheEntry entry = cacheByStamp.get(stamp);
if (entry != null) {
entry.lastSeen = new Instant();
if (!entry.deviceId.equals(deviceId)) {
// This probably happened because a device has changed it's ble mac address.
// We need to update the mac address for this entry.
cacheByDeviceId.remove(entry.deviceId);
entry.deviceId = deviceId;
cacheByDeviceId.put(deviceId, entry);
}
}
return entry != null;
}
}
/**
* Saves the set of advertisements and stamp for this device.
*
* @param stamp the stamp provided by the device
* @param deviceId the id of the device
* @param adinfos the advertisements exposed by the device
*/
void saveDevice(long stamp, String deviceId, Iterable<AdInfo> adInfos) {
Set<Equivalence.Wrapper<AdInfo>> newAdInfos =
FluentIterable.from(adInfos).transform(ADINFO_WRAPPER).toSet();
CacheEntry entry = new CacheEntry(stamp, deviceId, newAdInfos);
synchronized (this) {
Set<Equivalence.Wrapper<AdInfo>> oldAdInfos;
CacheEntry oldEntry = cacheByDeviceId.remove(deviceId);
if (oldEntry != null) {
cacheByStamp.remove(oldEntry.stamp);
oldAdInfos = oldEntry.adInfos;
} else {
oldAdInfos = ImmutableSet.of();
}
Set<Equivalence.Wrapper<AdInfo>> removed = Sets.difference(oldAdInfos, newAdInfos);
for (Equivalence.Wrapper<AdInfo> wrapped : removed) {
AdInfo adInfo = wrapped.get();
adInfosByInterfaceName.remove(adInfo.getAd().getInterfaceName(), wrapped);
adInfo.setLost(true);
handleUpdate(adInfo);
}
Set<Equivalence.Wrapper<AdInfo>> added = Sets.difference(newAdInfos, oldAdInfos);
for (Equivalence.Wrapper<AdInfo> wrapped : added) {
AdInfo adInfo = wrapped.get();
adInfosByInterfaceName.put(adInfo.getAd().getInterfaceName(), wrapped);
handleUpdate(adInfo);
}
cacheByStamp.put(stamp, entry);
cacheByDeviceId.put(deviceId, entry);
}
}
private void handleUpdate(AdInfo adinfo) {
Set<Scanner> scanners = scannersByInterfaceName.get("");
if (scanners != null) {
for (Scanner scanner : scanners) {
scanner.handler.handleUpdate(adinfo);
}
}
scanners = scannersByInterfaceName.get(adinfo.getAd().getInterfaceName());
if (scanners != null) {
for (Scanner scanner : scanners) {
scanner.handler.handleUpdate(adinfo);
}
}
}
/**
* Adds a scan handler for advertisements that match {@link interfaceName}.
* <p>
* If {@link handler} already exists, the old handler is replaced.
*/
void addScanner(String interfaceName, Plugin.ScanHandler handler) {
Scanner scanner = new Scanner(interfaceName, handler);
synchronized (this) {
scannersByHandler.put(handler, scanner);
scannersByInterfaceName.put(interfaceName, scanner);
Iterable<Equivalence.Wrapper<AdInfo>> adinfos;
if (interfaceName.isEmpty()) {
adinfos = adInfosByInterfaceName.values();
} else {
adinfos = adInfosByInterfaceName.get(interfaceName);
}
if (adinfos != null) {
for (Equivalence.Wrapper<AdInfo> wrapper : adinfos) {
scanner.handler.handleUpdate(wrapper.get());
}
}
}
}
/**
* Removes the scan handler.
*/
void removeScanner(Plugin.ScanHandler handler) {
synchronized (this) {
Scanner scanner = scannersByHandler.remove(handler);
if (scanner != null) {
scannersByInterfaceName.remove(scanner.interfaceName, scanner);
}
}
}
}