1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.camera.device;
18 
19 import com.android.camera.async.Lifetime;
20 import com.android.camera.async.SafeCloseable;
21 import com.android.camera.debug.Log.Tag;
22 import com.android.camera.debug.Logger;
23 
24 import java.util.concurrent.locks.ReentrantLock;
25 
26 import javax.annotation.Nullable;
27 import javax.annotation.ParametersAreNonnullByDefault;
28 import javax.annotation.concurrent.GuardedBy;
29 import javax.annotation.concurrent.ThreadSafe;
30 
31 /**
32  * Internal state machine for dealing with the interactions of opening
33  * a physical device. Since there are 4 device states and 2 target
34  * states the transition table looks like this:
35  *
36  * Device  | Target
37  * Opening   Opened -> Nothing.
38  * Opened    Opened -> Execute onDeviceOpened.
39  * Closing   Opened -> Nothing.
40  * Closed    Opened -> Execute onDeviceOpening.
41  *                     Device moves to Opening.
42  * Opening   Closed -> Nothing.
43  * Opened    Closed -> Execute onDeviceClosing.
44  *                     Device moves to Closing.
45  * Closing   Closed -> Nothing.
46  * Closed    Closed -> Execute onDeviceClosed.
47  *
48  */
49 @ThreadSafe
50 @ParametersAreNonnullByDefault
51 public class SingleDeviceStateMachine<TDevice, TKey> implements SingleDeviceCloseListener,
52       SingleDeviceOpenListener<TDevice> {
53     private static final Tag TAG = new Tag("DeviceStateM");
54 
55     /** Physical state of the device. */
56     private enum DeviceState {
57         OPENING,
58         OPENED,
59         CLOSING,
60         CLOSED
61     }
62 
63     /** Physical state the state machine should reach. */
64     private enum TargetState {
65         OPENED,
66         CLOSED
67     }
68 
69     private final ReentrantLock mLock;
70     private final Lifetime mDeviceLifetime;
71     private final SingleDeviceActions<TDevice> mDeviceActions;
72     private final SingleDeviceShutdownListener<TKey> mShutdownListener;
73     private final TKey mDeviceKey;
74     private final Logger mLogger;
75 
76     @GuardedBy("mLock")
77     private boolean mIsShutdown;
78 
79     @GuardedBy("mLock")
80     private TargetState mTargetState;
81 
82     @GuardedBy("mLock")
83     private DeviceState mDeviceState;
84 
85     @Nullable
86     @GuardedBy("mLock")
87     private SingleDeviceRequest<TDevice> mDeviceRequest;
88 
89     @Nullable
90     @GuardedBy("mLock")
91     private TDevice mOpenDevice;
92 
93     /**
94      * This creates a new state machine with a listener to represent
95      * the physical states of a device. Both the target and current
96      * state of the device are initially set to "Closed"
97      */
SingleDeviceStateMachine(SingleDeviceActions<TDevice> deviceActions, TKey deviceKey, SingleDeviceShutdownListener<TKey> deviceShutdownListener, Logger.Factory logFactory)98     public SingleDeviceStateMachine(SingleDeviceActions<TDevice> deviceActions,
99           TKey deviceKey, SingleDeviceShutdownListener<TKey> deviceShutdownListener,
100           Logger.Factory logFactory)  {
101         mDeviceActions = deviceActions;
102         mShutdownListener = deviceShutdownListener;
103         mDeviceKey = deviceKey;
104 
105         mLock = new ReentrantLock();
106         mDeviceLifetime = new Lifetime();
107         mLogger = logFactory.create(TAG);
108 
109         mIsShutdown = false;
110         mTargetState = TargetState.CLOSED;
111         mDeviceState = DeviceState.CLOSED;
112     }
113 
114     /**
115      * Request that the state machine move towards an open state.
116      */
requestOpen()117     public void requestOpen() {
118         mLock.lock();
119         try {
120             if (mIsShutdown) {
121                return;
122             }
123 
124             mTargetState = TargetState.OPENED;
125             update();
126         } finally {
127             mLock.unlock();
128         }
129     }
130 
131     /**
132      * Request that the state machine move towards a closed state.
133      */
requestClose()134     public void requestClose() {
135         mLock.lock();
136         try {
137             if (mIsShutdown) {
138                 return;
139             }
140 
141             mTargetState = TargetState.CLOSED;
142             update();
143         } finally {
144             mLock.unlock();
145         }
146     }
147 
148     /**
149      * When a new request is set, the previous request should be canceled
150      * if it has not been completed.
151      */
setRequest(final SingleDeviceRequest<TDevice> deviceRequest)152     public void setRequest(final SingleDeviceRequest<TDevice> deviceRequest) {
153         mLock.lock();
154         try {
155             if (mIsShutdown) {
156                 deviceRequest.close();
157                 return;
158             }
159 
160             SingleDeviceRequest<TDevice> previous = mDeviceRequest;
161             mDeviceRequest = deviceRequest;
162             mDeviceLifetime.add(deviceRequest);
163             deviceRequest.getLifetime().add(new SafeCloseable() {
164                     @Override
165                     public void close() {
166                         requestCloseIfCurrentRequest(deviceRequest);
167                     }
168                 });
169 
170             if (mOpenDevice != null) {
171                 mDeviceRequest.set(mOpenDevice);
172             }
173 
174             if (previous != null) {
175                 previous.close();
176             }
177         } finally {
178             mLock.unlock();
179         }
180     }
181 
182     @Override
onDeviceOpened(TDevice device)183     public void onDeviceOpened(TDevice device) {
184         mLock.lock();
185         try {
186             if (mIsShutdown) {
187                 return;
188             }
189 
190             mOpenDevice = device;
191             mDeviceState = DeviceState.OPENED;
192 
193             update();
194         } finally {
195             mLock.unlock();
196         }
197     }
198 
199     @Override
onDeviceOpenException(Throwable throwable)200     public void onDeviceOpenException(Throwable throwable) {
201         mLock.lock();
202         try {
203             if (mIsShutdown) {
204                 return;
205             }
206 
207             closeRequestWithException(throwable);
208             shutdown();
209         } finally {
210             mLock.unlock();
211         }
212     }
213 
214     @Override
onDeviceOpenException(TDevice tDevice)215     public void onDeviceOpenException(TDevice tDevice) {
216         mLock.lock();
217         try {
218             if (mIsShutdown) {
219                 return;
220             }
221 
222             closeRequestWithException(new CameraOpenException(-1));
223             mDeviceState = DeviceState.CLOSING;
224             mTargetState = TargetState.CLOSED;
225             executeClose(tDevice);
226         } finally {
227             mLock.unlock();
228         }
229     }
230 
231     @Override
onDeviceClosed()232     public void onDeviceClosed() {
233         mLock.lock();
234         try {
235             if (mIsShutdown) {
236                 return;
237             }
238 
239             mOpenDevice = null;
240             mDeviceState = DeviceState.CLOSED;
241 
242             update();
243         } finally {
244             mLock.unlock();
245         }
246     }
247 
248     @Override
onDeviceClosingException(Throwable throwable)249     public void onDeviceClosingException(Throwable throwable) {
250         mLock.lock();
251         try {
252             if (mIsShutdown) {
253                 return;
254             }
255 
256             closeRequestWithException(throwable);
257             shutdown();
258         } finally {
259             mLock.unlock();
260         }
261     }
262 
263 
264     @GuardedBy("mLock")
update()265     private void update() {
266         if (mIsShutdown) {
267             return;
268         }
269 
270         if (mDeviceState == DeviceState.CLOSED && mTargetState == TargetState.OPENED) {
271             executeOpen();
272         } else if (mDeviceState == DeviceState.OPENED && mTargetState == TargetState.OPENED) {
273             executeOpened();
274         } else if (mDeviceState == DeviceState.OPENED && mTargetState == TargetState.CLOSED) {
275             executeClose();
276         }  else if (mDeviceState == DeviceState.CLOSED && mTargetState == TargetState.CLOSED) {
277             shutdown();
278         }
279     }
280 
281     @GuardedBy("mLock")
executeOpen()282     private void executeOpen() {
283         mDeviceState = DeviceState.OPENING;
284         try {
285             mDeviceActions.executeOpen(this, mDeviceLifetime);
286         } catch (Exception e) {
287             onDeviceOpenException(e);
288         }
289         // TODO: Consider adding a timeout to the open call so that requests
290         // are not left un-resolved.
291     }
292 
293     @GuardedBy("mLock")
executeOpened()294     private void executeOpened() {
295         if(mDeviceRequest != null) {
296             mDeviceRequest.set(mOpenDevice);
297         }
298 
299         // TODO: Consider performing a shutdown if there is no open
300         // device request.
301     }
302 
303     @GuardedBy("mLock")
executeClose()304     private void executeClose() {
305         // TODO: Consider adding a timeout to the close call so that requests
306         // are not left un-resolved.
307 
308         final TDevice device = mOpenDevice;
309         mOpenDevice = null;
310 
311         executeClose(device);
312     }
313 
314     @GuardedBy("mLock")
executeClose(@ullable TDevice device)315     private void executeClose(@Nullable TDevice device) {
316         if (device != null) {
317             mDeviceState = DeviceState.CLOSING;
318             mTargetState = TargetState.CLOSED;
319             closeRequest();
320 
321             try {
322                 mDeviceActions.executeClose(this, device);
323             } catch (Exception e) {
324                 onDeviceClosingException(e);
325             }
326         } else {
327             shutdown();
328         }
329     }
330 
331     @GuardedBy("mLock")
requestCloseIfCurrentRequest(SingleDeviceRequest<TDevice> request)332     private void requestCloseIfCurrentRequest(SingleDeviceRequest<TDevice> request) {
333         if (mDeviceRequest == null || mDeviceRequest == request) {
334             requestClose();
335         }
336     }
337 
338     @GuardedBy("mLock")
closeRequestWithException(Throwable exception)339     private void closeRequestWithException(Throwable exception) {
340         mOpenDevice = null;
341         if (mDeviceRequest != null) {
342             mLogger.w("There was a problem closing device: " + mDeviceKey, exception);
343 
344             mDeviceRequest.closeWithException(exception);
345             mDeviceRequest = null;
346         }
347     }
348 
349     @GuardedBy("mLock")
closeRequest()350     private void closeRequest() {
351         if (mDeviceRequest != null) {
352             mDeviceRequest.close();
353         }
354         mDeviceRequest = null;
355     }
356 
357     /**
358      * Cancel requests, and set internal device state back to
359      * a clean set of values.
360      */
shutdown()361     private void shutdown() {
362         mLock.lock();
363         try {
364             if (!mIsShutdown) {
365                 mIsShutdown = true;
366                 mLogger.i("Shutting down the device lifecycle for: " + mDeviceKey);
367                 mOpenDevice = null;
368                 mDeviceState = DeviceState.CLOSED;
369                 mTargetState = TargetState.CLOSED;
370 
371                 closeRequest();
372                 mDeviceLifetime.close();
373                 mShutdownListener.onShutdown(mDeviceKey);
374             } else {
375                 mLogger.w("Shutdown was called multiple times!");
376             }
377         } finally {
378             mLock.unlock();
379         }
380     }
381 }
382