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
17package com.android.camera.device;
18
19import com.android.camera.async.Lifetime;
20import com.android.camera.async.SafeCloseable;
21import com.android.camera.debug.Log.Tag;
22import com.android.camera.debug.Logger;
23
24import java.util.concurrent.locks.ReentrantLock;
25
26import javax.annotation.Nullable;
27import javax.annotation.ParametersAreNonnullByDefault;
28import javax.annotation.concurrent.GuardedBy;
29import 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
51public 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     */
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     */
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     */
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     */
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
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
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
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
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
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")
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")
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")
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")
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")
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")
332    private void requestCloseIfCurrentRequest(SingleDeviceRequest<TDevice> request) {
333        if (mDeviceRequest == null || mDeviceRequest == request) {
334            requestClose();
335        }
336    }
337
338    @GuardedBy("mLock")
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")
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     */
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