]> git.basschouten.com Git - openhab-addons.git/blob
78ec6838bbcc3733b39e3f4cc43c67ed9d28c966
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth.bluez.internal;
14
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;
21
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;
31
32 import com.github.hypfvieh.bluetooth.DeviceManager;
33
34 /**
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.
38  *
39  * Creation of the DeviceManagerWrapper is asynchronous and thus attempts to retrieve the
40  * DeviceManagerWrapper through 'getDeviceManager' may initially fail.
41  *
42  * @author Connor Petty - Initial Contribution
43  *
44  */
45 @NonNullByDefault
46 @Component(service = DeviceManagerFactory.class)
47 public class DeviceManagerFactory {
48
49     private final Logger logger = LoggerFactory.getLogger(DeviceManagerFactory.class);
50     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
51
52     private final BlueZPropertiesChangedHandler changeHandler = new BlueZPropertiesChangedHandler();
53
54     private @Nullable CompletableFuture<DeviceManager> deviceManagerFuture;
55     private @Nullable CompletableFuture<DeviceManagerWrapper> deviceManagerWrapperFuture;
56
57     public BlueZPropertiesChangedHandler getPropertiesChangedHandler() {
58         return changeHandler;
59     }
60
61     public @Nullable DeviceManagerWrapper getDeviceManager() {
62         // we can cheat the null checker with casting here
63         var future = (CompletableFuture<@Nullable DeviceManagerWrapper>) deviceManagerWrapperFuture;
64         if (future != null) {
65             return future.getNow(null);
66         }
67         return null;
68     }
69
70     @Activate
71     public void initialize() {
72         logger.debug("initializing DeviceManagerFactory");
73
74         var stage1 = this.deviceManagerFuture = callAsync(() -> {
75             try {
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);
83             }
84         }, scheduler);
85
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();
93                 try {
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) {
99                     if (count < 3) {
100                         throw new RetryException(5, TimeUnit.SECONDS);
101                     } else {
102                         throw e;
103                     }
104                 }
105             }, scheduler);
106         }).whenComplete((devManagerWrapper, th) -> {
107             if (th != null) {
108                 logger.warn("Failed to initialize DeviceManager: {}", th.getMessage());
109             }
110         });
111     }
112
113     @Deactivate
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);
120             }
121         }
122         this.deviceManagerFuture = null;
123
124         var stage2 = this.deviceManagerWrapperFuture;
125         if (stage2 != null) {
126             stage2.cancel(true);
127         }
128         this.deviceManagerWrapperFuture = null;
129     }
130
131     private static <T> CompletableFuture<T> callAsync(Callable<T> callable, ScheduledExecutorService scheduler) {
132         return new AsyncCompletableFuture<>(callable, scheduler);
133     }
134
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 {
138
139         private final Callable<T> callable;
140         private final ScheduledExecutorService scheduler;
141         private final Object futureLock = new Object();
142         private Future<?> future;
143
144         public AsyncCompletableFuture(Callable<T> callable, ScheduledExecutorService scheduler) {
145             this.callable = callable;
146             this.scheduler = scheduler;
147             future = scheduler.submit(this);
148         }
149
150         @Override
151         public boolean cancel(boolean mayInterruptIfRunning) {
152             synchronized (futureLock) {
153                 future.cancel(mayInterruptIfRunning);
154             }
155             return super.cancel(mayInterruptIfRunning);
156         }
157
158         @Override
159         public void run() {
160             try {
161                 complete(callable.call());
162             } catch (RetryException e) {
163                 synchronized (futureLock) {
164                     if (!future.isCancelled()) {
165                         future = scheduler.schedule(this, e.delay, e.unit);
166                     }
167                 }
168             } catch (Exception e) {
169                 completeExceptionally(e);
170             }
171         }
172     }
173
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 {
176
177         private static final long serialVersionUID = 8512275408512109328L;
178         private long delay;
179         private TimeUnit unit;
180
181         public RetryException(long delay, TimeUnit unit) {
182             this.delay = delay;
183             this.unit = unit;
184         }
185     }
186 }