All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sputnikdev.bluetooth.manager.impl.BluetoothManagerImpl Maven / Gradle / Ivy

package org.sputnikdev.bluetooth.manager.impl;

/*-
 * #%L
 * org.sputnikdev:bluetooth-manager
 * %%
 * Copyright (C) 2017 Sputnik Dev
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sputnikdev.bluetooth.URL;
import org.sputnikdev.bluetooth.manager.AdapterDiscoveryListener;
import org.sputnikdev.bluetooth.manager.AdapterGovernor;
import org.sputnikdev.bluetooth.manager.BluetoothGovernor;
import org.sputnikdev.bluetooth.manager.BluetoothManager;
import org.sputnikdev.bluetooth.manager.CharacteristicGovernor;
import org.sputnikdev.bluetooth.manager.CombinedGovernor;
import org.sputnikdev.bluetooth.manager.DeviceDiscoveryListener;
import org.sputnikdev.bluetooth.manager.DeviceGovernor;
import org.sputnikdev.bluetooth.manager.DiscoveredAdapter;
import org.sputnikdev.bluetooth.manager.DiscoveredDevice;
import org.sputnikdev.bluetooth.manager.ManagerListener;
import org.sputnikdev.bluetooth.manager.transport.BluetoothObject;
import org.sputnikdev.bluetooth.manager.transport.BluetoothObjectFactory;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Thread safe bluetooth manager implementation class.
 * @author Vlad Kolotov
 */
class BluetoothManagerImpl implements BluetoothManager {

    private static final int REFRESH_RATE_SEC = 5;
    private static final int DISCOVERY_RATE_SEC = 10;

    private Logger logger = LoggerFactory.getLogger(BluetoothManagerImpl.class);

    private final ScheduledExecutorService discoveryScheduler = Executors.newScheduledThreadPool(6);
    private final ScheduledExecutorService governorScheduler = Executors.newScheduledThreadPool(5);
    private final Map> adapterDiscoveryFutures = new ConcurrentHashMap<>();
    private final Map> deviceDiscoveryFutures = new ConcurrentHashMap<>();
    private final Map> governorFutures = new HashMap<>();

    private final Set deviceDiscoveryListeners = new CopyOnWriteArraySet<>();
    private final Set adapterDiscoveryListeners = new CopyOnWriteArraySet<>();
    private final Set managerListeners = new CopyOnWriteArraySet<>();

    private final Map governors = new ConcurrentHashMap<>();
    private final Set discoveredDevices = new CopyOnWriteArraySet<>();
    private final Set discoveredAdapters = new CopyOnWriteArraySet<>();

    private boolean startDiscovering;
    private int discoveryRate = DISCOVERY_RATE_SEC;
    private int refreshRate = REFRESH_RATE_SEC;
    private boolean rediscover;
    private boolean started;
    private boolean combinedAdapters;
    private boolean combinedDevices = true;

    @Override
    public void start(boolean startDiscovering) {
        if (started || !adapterDiscoveryFutures.isEmpty() || !deviceDiscoveryFutures.isEmpty()
            || !governorFutures.isEmpty()) {
            return;
        }
        this.startDiscovering = startDiscovering;
        synchronized (discoveryScheduler) {
            BluetoothObjectFactoryProvider.getRegisteredFactories().forEach(this::scheduleDiscovery);
        }
        synchronized (governorScheduler) {
            governors.values().forEach(this::scheduleGovernor);
        }
        started = true;
    }

    @Override
    public void stop() {
        cancelAllFutures(false);
        started = false;
    }

    @Override
    public boolean isStarted() {
        return started;
    }

    @Override
    public void addDeviceDiscoveryListener(DeviceDiscoveryListener deviceDiscoveryListener) {
        deviceDiscoveryListeners.add(deviceDiscoveryListener);
    }

    @Override
    public void removeDeviceDiscoveryListener(DeviceDiscoveryListener deviceDiscoveryListener) {
        deviceDiscoveryListeners.remove(deviceDiscoveryListener);
    }

    @Override
    public void addAdapterDiscoveryListener(AdapterDiscoveryListener adapterDiscoveryListener) {
        adapterDiscoveryListeners.add(adapterDiscoveryListener);
    }

    @Override
    public void removeAdapterDiscoveryListener(AdapterDiscoveryListener adapterDiscoveryListener) {
        adapterDiscoveryListeners.remove(adapterDiscoveryListener);
    }

    @Override
    public void disposeGovernor(URL url) {
        computeIfGovernorPresent(url, (protocolLess, governor) -> {
            disposeGovernor(governor);
            return null;
        });
    }

    @Override
    public void disposeDescendantGovernors(URL url) {
        computeForEachDescendantGovernorAndRemove(url, this::disposeGovernor);
    }

    @Override
    public AdapterGovernor getAdapterGovernor(URL url) {
        return (AdapterGovernor) getGovernor(url.getAdapterURL());
    }

    @Override
    public DeviceGovernor getDeviceGovernor(URL url) {
        return (DeviceGovernor) getGovernor(url.getDeviceURL());
    }

    @Override
    public DeviceGovernor getDeviceGovernorAutoconnect(URL url) {
        DeviceGovernor deviceGovernor = getDeviceGovernor(url);
        deviceGovernor.setConnectionControl(true);
        return deviceGovernor;
    }

    @Override
    public CharacteristicGovernor getCharacteristicGovernor(URL url) {
        return (CharacteristicGovernor) getGovernor(url.getCharacteristicURL());
    }

    @Override
    public CharacteristicGovernor getCharacteristicGovernorAutoconnect(URL url) {
        getDeviceGovernor(url).setConnectionControl(true);
        return getCharacteristicGovernor(url);
    }

    @Override
    public void dispose() {
        logger.info("Disposing Bluetooth manager");

        cancelAllFutures(true);

        governorScheduler.shutdown();
        discoveryScheduler.shutdown();

        deviceDiscoveryListeners.clear();
        adapterDiscoveryListeners.clear();

        governors.values().forEach(this::dispose);
        governors.clear();

        logger.info("Bluetooth service has been disposed");
    }

    @Override
    public Set getDiscoveredDevices() {
        if (combinedDevices) {
            Map> groupedByDeviceAddress =
                discoveredDevices.stream().collect(
                    Collectors.groupingBy(t -> t.getURL().copyWithAdapter(CombinedGovernor.COMBINED_ADDRESS)));
            return groupedByDeviceAddress.entrySet().stream().map(entry -> {
                DiscoveredDevice discoveredDevice = entry.getValue().get(0);
                return new DiscoveredDevice(entry.getKey(), discoveredDevice.getName(), discoveredDevice.getAlias(),
                    discoveredDevice.isBleEnabled());
            }).collect(Collectors.toSet());
        } else {
            return Collections.unmodifiableSet(discoveredDevices);
        }
    }

    @Override
    public Set getDiscoveredAdapters() {
        if (combinedAdapters) {
            return discoveredAdapters.stream().map(adapter -> {
                return new DiscoveredAdapter(new URL("/" + CombinedGovernor.COMBINED_ADDRESS),
                        adapter.getName(), adapter.getAlias());
            }).collect(Collectors.toSet());
        } else {
            return Collections.unmodifiableSet(discoveredAdapters);
        }
    }

    @Override
    public BluetoothGovernor getGovernor(URL url) {
        if (url.isProtocol() || url.isRoot()) {
            return null;
        }
        return computeIfGovernorAbsent(url, protocolLess -> {
            BluetoothObjectGovernor governor = createGovernor(protocolLess);

            scheduleGovernor(governor);

            governorScheduler.submit(() -> {
                init(governor);
            });

            return governor;
        });
    }

    @Override
    public void setDiscoveryRate(int discoveryRate) {
        this.discoveryRate = discoveryRate;
    }

    @Override
    public void setRediscover(boolean rediscover) {
        this.rediscover = rediscover;
    }

    @Override
    public void setRefreshRate(int refreshRate) {
        this.refreshRate = refreshRate;
    }

    @Override
    public void enableCombinedAdapters(boolean combineAdapters) {
        combinedAdapters = combineAdapters;
    }

    @Override
    public void enableCombinedDevices(boolean combineDevices) {
        combinedDevices = combineDevices;
    }

    @Override
    public boolean isCombinedAdaptersEnabled() {
        return combinedAdapters;
    }

    @Override
    public boolean isCombinedDevicesEnabled() {
        return combinedDevices;
    }

    @Override
    public void addManagerListener(ManagerListener listener) {
        managerListeners.add(listener);
    }

    @Override
    public void removeManagerListener(ManagerListener listener) {
        managerListeners.remove(listener);
    }

    protected void notifyGovernorReady(BluetoothGovernor governor, boolean ready) {
        BluetoothManagerUtils.safeForEachError(managerListeners, listener -> listener.ready(governor, ready), logger,
                "Error in manager listener: ready");
    }

    List getGovernors(List objects) {
        return Collections.unmodifiableList(objects.stream()
            .map(o -> getGovernor(o.getURL())).collect(Collectors.toList()));
    }

    void updateDescendants(URL parent) {
        computeForEachDescendantGovernor(parent, this::update);
    }

    void resetDescendants(URL parent) {
        if (parent.isProtocol()) {
            // reset all governors that belongs to the transport specified in the argument
            governors.values().stream().filter(governor -> governor instanceof AbstractBluetoothObjectGovernor)
                .map(governor -> (AbstractBluetoothObjectGovernor) governor)
                .filter(governor -> parent.getProtocol().equals(governor.getTransport()))
                .forEach(this::reset);
        } else {
            computeForEachDescendantGovernor(parent, this::reset);
        }
    }

    /**
     * This is a very centric method that returns a "native" objects. Mostly used by governors to acquire
     * a corresponding native object.
     * @param url bluetooth url
     * @return a native object corresponding to the given url
     */
     T getBluetoothObject(URL url) {
        BluetoothObjectFactory factory = findFactory(url);
        BluetoothObject bluetoothObject = null;
        if (factory != null) {
            URL objectURL = url.copyWithProtocol(factory.getProtocolName());
            if (objectURL.isAdapter()) {
                bluetoothObject = factory.getAdapter(objectURL);
            } else if (objectURL.isDevice()) {
                bluetoothObject = factory.getDevice(objectURL);
            } else if (objectURL.isCharacteristic()) {
                bluetoothObject = factory.getCharacteristic(objectURL);
            }
        }
        return (T) bluetoothObject;
    }

    BluetoothObjectGovernor createGovernor(URL url) {
        if (CombinedGovernor.COMBINED_ADDRESS.equals(url.getAdapterAddress())) {
            return createCombinedGovernor(url);
        } else {
            return createBasicGovernor(url);
        }
    }

    private BluetoothObjectGovernor createCombinedGovernor(URL url) {
        if (url.isAdapter()) {
            AdapterGovernor adapterGovernor = new CombinedAdapterGovernorImpl(this, url);
            adapterGovernor.setDiscoveringControl(startDiscovering);
            return (BluetoothObjectGovernor) adapterGovernor;
        } else if (url.isDevice()) {
            return new CombinedDeviceGovernorImpl(this, url);
        } else if (url.isCharacteristic()) {
            return new CombinedCharacteristicGovernorImpl(this, url);
        }
        throw new IllegalStateException("Unknown url");
    }

    private BluetoothObjectGovernor createBasicGovernor(URL url) {
        if (url.isAdapter()) {
            AdapterGovernor adapterGovernor = new AdapterGovernorImpl(this, url);
            adapterGovernor.setDiscoveringControl(startDiscovering);
            return (BluetoothObjectGovernor) adapterGovernor;
        } else if (url.isDevice()) {
            return new DeviceGovernorImpl(this, url);
        } else if (url.isCharacteristic()) {
            return new CharacteristicGovernorImpl(this, url);
        }
        throw new IllegalStateException("Unknown url");
    }

    void handleObjectFactoryRegistered(BluetoothObjectFactory bluetoothObjectFactory) {
        scheduleDiscovery(bluetoothObjectFactory);
    }

    void handleObjectFactoryUnregistered(BluetoothObjectFactory bluetoothObjectFactory) {
        String protocol = bluetoothObjectFactory.getProtocolName();
        synchronized (discoveryScheduler) {
            cancelFutures(adapterDiscoveryFutures, protocol);
            cancelFutures(deviceDiscoveryFutures, protocol);
        }
        resetDescendants(new URL().copyWithProtocol(protocol));
    }

    Set getRegisteredGovernors() {
        return Collections.unmodifiableSet(governors.keySet());
    }

    private void disposeGovernor(BluetoothObjectGovernor governor) {
        governorFutures.computeIfPresent(governor.getURL(), (url, future) -> {
            future.cancel(true);
            return null;
        });
        dispose(governor);
    }

    private BluetoothObjectFactory findFactory(URL url) {
        String protocol = url.getProtocol();
        String adapterAddress = url.getAdapterAddress();
        if (url.getProtocol() != null) {
            return BluetoothObjectFactoryProvider.getFactory(protocol);
        } else {
            for (DiscoveredAdapter adapter : discoveredAdapters) {
                if (adapter.getURL().getAdapterAddress().equals(adapterAddress)) {
                    return BluetoothObjectFactoryProvider.getFactory(adapter.getURL().getProtocol());
                }
            }
        }
        return null;
    }

    private void notifyDeviceDiscovered(DiscoveredDevice device) {
        if (discoveredDevices.contains(device) && !rediscover) {
            return;
        }
        wrapForEach(deviceDiscoveryListeners, listener -> {
            if (!combinedDevices || listener instanceof CombinedDeviceGovernorImpl) {
                listener.discovered(device);
            } else {
                listener.discovered(new DiscoveredDevice(
                        device.getURL().copyWithAdapter(CombinedGovernor.COMBINED_ADDRESS),
                        device.getName(), device.getAlias(), device.isBleEnabled()));
            }
        },"Error in device discovery listener");
    }

    private void notifyAdapterDiscovered(DiscoveredAdapter adapter) {
        if (discoveredAdapters.contains(adapter) && !rediscover) {
            return;
        }
        wrapForEach(adapterDiscoveryListeners, listener -> {
            if (!combinedAdapters || listener instanceof CombinedAdapterGovernorImpl) {
                listener.discovered(adapter);
            } else {
                listener.discovered(new DiscoveredAdapter(new URL("/" + CombinedGovernor.COMBINED_ADDRESS),
                    "Combined Bluetooth Adapter", null));
            }
        },"Error in adapter discovery listener");
    }

    private void handleDeviceLost(URL url) {
        logger.info("Device has been lost: " + url);
        wrapForEach(deviceDiscoveryListeners, deviceDiscoveryListener -> deviceDiscoveryListener.deviceLost(url),
            "Error in device discovery listener");
    }

    private void handleAdapterLost(URL url) {
        logger.info("Adapter has been lost: " + url);
        wrapForEach(adapterDiscoveryListeners, adapterDiscoveryListener -> adapterDiscoveryListener.adapterLost(url),
            "Error in adapter discovery listener");
        reset((BluetoothObjectGovernor) getAdapterGovernor(url));
    }

    private void reset(BluetoothObjectGovernor governor) {
        try {
            governor.reset();
        } catch (Exception ex) {
            logger.error("Could not reset governor: " + governor, ex);
        }
    }

    private void dispose(BluetoothObjectGovernor governor) {
        try {
            governor.dispose();
        } catch (Exception ex) {
            logger.error("Could not dispose governor: " + governor, ex);
        }
    }

    private void update(BluetoothObjectGovernor governor) {
        try {
            logger.debug("Updating governor: {}", governor.getURL());
            governor.update();
        } catch (Exception ex) {
            logger.warn("Could not update governor: " + governor, ex);
        }
    }

    private void init(BluetoothObjectGovernor governor) {
        try {
            governor.init();
        } catch (Exception ex) {
            logger.warn("Could not init governor: " + governor, ex);
        }
    }

    private final class DeviceDiscoveryJob implements Runnable {

        private final BluetoothObjectFactory factory;

        private DeviceDiscoveryJob(BluetoothObjectFactory factory) {
            this.factory = factory;
        }

        @Override
        public void run() {
            try {
                discoverDevices();
            } catch (Exception ex) {
                logger.error("Device discovery job error", ex);
            }
        }

        private void discoverDevices() {
            Set discovered = factory.getDiscoveredDevices().stream()
                .filter(device -> device.getRSSI() != 0).collect(Collectors.toSet());

            discovered.forEach(BluetoothManagerImpl.this::notifyDeviceDiscovered);

            Set factoryDevices = discoveredDevices.stream()
                    .filter(device -> factory.getProtocolName().equals(device.getURL().getProtocol()))
                    .collect(Collectors.toSet());

            Set lostDevices = Sets.difference(factoryDevices, discovered);
            lostDevices.forEach(lost -> handleDeviceLost(lost.getURL()));

            discoveredDevices.removeAll(lostDevices);
            discoveredDevices.addAll(discovered);
        }
    }

    private final class AdapterDiscoveryJob implements Runnable {

        private final BluetoothObjectFactory factory;

        private AdapterDiscoveryJob(BluetoothObjectFactory factory) {
            this.factory = factory;
        }

        @Override
        public void run() {
            try {
                discoverAdapters();
            } catch (Exception ex) {
                logger.error("Adapter discovery job error", ex);
            }
        }

        private void discoverAdapters() {
            Set discovered = new HashSet<>(factory.getDiscoveredAdapters());

            discovered.forEach(adapter -> {
                notifyAdapterDiscovered(adapter);
                if (startDiscovering) {
                    // create (if not created before) adapter governor which will trigger its discovering status
                    // (by default when it is created "discovering" flag is set to true)
                    getAdapterGovernor(adapter.getURL());
                }
            });

            Set factoryAdapters = discoveredAdapters.stream()
                    .filter(device -> factory.getProtocolName().equals(device.getURL().getProtocol()))
                    .collect(Collectors.toSet());

            Set lostAdapters = Sets.difference(factoryAdapters, discovered);
            lostAdapters.forEach(lost -> handleAdapterLost(lost.getURL()));

            discoveredAdapters.removeAll(lostAdapters);
            discoveredAdapters.addAll(discovered);
        }
    }

    private BluetoothObjectGovernor computeIfGovernorPresent(URL url,
                                          BiFunction function) {
        return governors.computeIfPresent(url.copyWithProtocol(null), function);
    }

    private BluetoothObjectGovernor computeIfGovernorAbsent(URL url, Function supplier) {
        return governors.computeIfAbsent(url.copyWithProtocol(null), supplier);
    }

    private void computeForEachDescendantGovernorAndRemove(URL url, Consumer consumer) {
        URL protocolLess = url.copyWithProtocol(null);
        governors.entrySet().removeIf(entry -> {
            if (entry.getKey().isDescendant(protocolLess)) {
                consumer.accept(entry.getValue());
                return true;
            }
            return false;
        });
    }

    private void computeForEachDescendantGovernor(URL url, Consumer consumer) {
        URL protocolLess = url.copyWithProtocol(null);
        governors.values().stream().filter(governor -> governor.getURL().isDescendant(protocolLess)).forEach(consumer);
    }

    private  void wrapForEach(Set listeners, Consumer func, String error) {
        listeners.forEach(deviceDiscoveryListener -> {
            try {
                func.accept(deviceDiscoveryListener);
            } catch (Exception ex) {
                logger.error(error, ex);
            }
        });
    }

    private void scheduleDiscovery(BluetoothObjectFactory factory) {
        adapterDiscoveryFutures.put(factory.getProtocolName(),
            discoveryScheduler.scheduleWithFixedDelay(
                new AdapterDiscoveryJob(factory), 0, discoveryRate, TimeUnit.SECONDS));

        deviceDiscoveryFutures.put(factory.getProtocolName(),
            discoveryScheduler.scheduleWithFixedDelay(
                new DeviceDiscoveryJob(factory), 5, discoveryRate, TimeUnit.SECONDS));
    }

    private void scheduleGovernor(BluetoothObjectGovernor governor) {
        governorFutures.put(governor.getURL(),
            governorScheduler.scheduleWithFixedDelay(() -> update(governor),5, refreshRate, TimeUnit.SECONDS));
    }

    private void cancelAllFutures(boolean forceInterrupt) {
        synchronized (discoveryScheduler) {
            adapterDiscoveryFutures.values().forEach(future -> future.cancel(forceInterrupt));
            adapterDiscoveryFutures.clear();
            deviceDiscoveryFutures.values().forEach(future -> future.cancel(forceInterrupt));
            deviceDiscoveryFutures.clear();
        }
        synchronized (governorScheduler) {
            governorFutures.values().forEach(future -> future.cancel(forceInterrupt));
            governorFutures.clear();
        }
    }

    private static void cancelFutures(Map> futures, String transport) {
        futures.entrySet().removeIf(entry -> {
            if (entry.getKey().equals(transport)) {
                entry.getValue().cancel(true);
                return true;
            }
            return false;
        });
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy