2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.bluez.internal;
15 import java.util.concurrent.Callable;
16 import java.util.concurrent.CompletableFuture;
17 import java.util.concurrent.Future;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.TimeUnit;
20 import java.util.concurrent.atomic.AtomicInteger;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.freedesktop.dbus.exceptions.DBusException;
25 import org.openhab.core.common.ThreadPoolManager;
26 import org.osgi.service.component.annotations.Activate;
27 import org.osgi.service.component.annotations.Component;
28 import org.osgi.service.component.annotations.Deactivate;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import com.github.hypfvieh.bluetooth.DeviceManager;
35 * This service handles the lifecycle of the {@link DeviceManager} singleton instance.
36 * In addition, this class is responsible for managing the BlueZPropertiesChangedHandler instance
37 * used by the binding for listening and dispatching dbus events from the DeviceManager.
39 * Creation of the DeviceManagerWrapper is asynchronous and thus attempts to retrieve the
40 * DeviceManagerWrapper through 'getDeviceManager' may initially fail.
42 * @author Connor Petty - Initial Contribution
46 @Component(service = DeviceManagerFactory.class)
47 public class DeviceManagerFactory {
49 private final Logger logger = LoggerFactory.getLogger(DeviceManagerFactory.class);
50 private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
52 private final BlueZPropertiesChangedHandler changeHandler = new BlueZPropertiesChangedHandler();
54 private @Nullable CompletableFuture<DeviceManager> deviceManagerFuture;
55 private @Nullable CompletableFuture<DeviceManagerWrapper> deviceManagerWrapperFuture;
57 public BlueZPropertiesChangedHandler getPropertiesChangedHandler() {
61 public @Nullable DeviceManagerWrapper getDeviceManager() {
62 // we can cheat the null checker with casting here
63 var future = (CompletableFuture<@Nullable DeviceManagerWrapper>) deviceManagerWrapperFuture;
65 return future.getNow(null);
71 public void initialize() {
72 logger.debug("initializing DeviceManagerFactory");
74 var stage1 = this.deviceManagerFuture = callAsync(() -> {
76 // if this is the first call to the library, this call
77 // should throw an exception (that we are catching)
78 return DeviceManager.getInstance();
79 // Experimental - seems reuse does not work
80 } catch (IllegalStateException e) {
81 // Exception caused by first call to the library
82 return DeviceManager.createInstance(false);
86 stage1.thenCompose(devManager -> {
87 // lambdas can't modify outside variables due to scoping, so instead we use an AtomicInteger.
88 AtomicInteger tryCount = new AtomicInteger();
89 // We need to set deviceManagerWrapperFuture here since we want to be able to cancel the underlying
90 // AsyncCompletableFuture instance
91 return this.deviceManagerWrapperFuture = callAsync(() -> {
92 int count = tryCount.incrementAndGet();
94 logger.debug("Registering property handler attempt: {}", count);
95 devManager.registerPropertyHandler(changeHandler);
96 logger.debug("Successfully registered property handler");
97 return new DeviceManagerWrapper(devManager);
98 } catch (DBusException e) {
100 throw new RetryException(5, TimeUnit.SECONDS);
106 }).whenComplete((devManagerWrapper, th) -> {
108 logger.warn("Failed to initialize DeviceManager: {}", th.getMessage());
114 public void dispose() {
115 var stage1 = this.deviceManagerFuture;
116 if (stage1 != null) {
117 if (!stage1.cancel(true)) {
118 // a failure to cancel means that the stage completed normally
119 stage1.thenAccept(DeviceManager::closeConnection);
122 this.deviceManagerFuture = null;
124 var stage2 = this.deviceManagerWrapperFuture;
125 if (stage2 != null) {
128 this.deviceManagerWrapperFuture = null;
131 private static <T> CompletableFuture<T> callAsync(Callable<T> callable, ScheduledExecutorService scheduler) {
132 return new AsyncCompletableFuture<>(callable, scheduler);
135 // this is a utility class that allows use of Callable with CompletableFutures in a way such that the
136 // async future is cancellable thru this CompletableFuture instance.
137 private static class AsyncCompletableFuture<T> extends CompletableFuture<T> implements Runnable {
139 private final Callable<T> callable;
140 private final ScheduledExecutorService scheduler;
141 private final Object futureLock = new Object();
142 private Future<?> future;
144 public AsyncCompletableFuture(Callable<T> callable, ScheduledExecutorService scheduler) {
145 this.callable = callable;
146 this.scheduler = scheduler;
147 future = scheduler.submit(this);
151 public boolean cancel(boolean mayInterruptIfRunning) {
152 synchronized (futureLock) {
153 future.cancel(mayInterruptIfRunning);
155 return super.cancel(mayInterruptIfRunning);
161 complete(callable.call());
162 } catch (RetryException e) {
163 synchronized (futureLock) {
164 if (!future.isCancelled()) {
165 future = scheduler.schedule(this, e.delay, e.unit);
168 } catch (Exception e) {
169 completeExceptionally(e);
174 // this is a special exception to indicate to a AsyncCompletableFuture that the task needs to be retried.
175 private static class RetryException extends Exception {
177 private static final long serialVersionUID = 8512275408512109328L;
179 private TimeUnit unit;
181 public RetryException(long delay, TimeUnit unit) {