1/*
2 * Copyright 2013 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 */
16package com.android.ex.camera2.blocking;
17
18import android.hardware.camera2.CameraDevice;
19import android.os.Handler;
20import android.os.SystemClock;
21import android.util.Log;
22
23import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
24
25import java.util.Arrays;
26import java.util.Collection;
27import java.util.concurrent.LinkedBlockingQueue;
28import java.util.concurrent.TimeUnit;
29
30
31/**
32 * A camera device listener that implements blocking operations on state changes.
33 *
34 * <p>Provides wait calls that block until the next unobserved state of the
35 * requested type arrives. Unobserved states are states that have occurred since
36 * the last wait, or that will be received from the camera device in the
37 * future.</p>
38 *
39 * <p>Pass-through all StateCallback changes to the proxy.</p>
40 *
41 */
42public class BlockingStateCallback extends CameraDevice.StateCallback {
43    private static final String TAG = "BlockingStateCallback";
44    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
45
46    private final CameraDevice.StateCallback mProxy;
47
48    // Guards mWaiting
49    private final Object mLock = new Object();
50    private boolean mWaiting = false;
51
52    private final LinkedBlockingQueue<Integer> mRecentStates =
53            new LinkedBlockingQueue<Integer>();
54
55    private void setCurrentState(int state) {
56        if (VERBOSE) Log.v(TAG, "Camera device state now " + stateToString(state));
57        try {
58            mRecentStates.put(state);
59        } catch (InterruptedException e) {
60            throw new RuntimeException("Unable to set device state", e);
61        }
62    }
63
64    private static final String[] mStateNames = {
65        "STATE_UNINITIALIZED",
66        "STATE_OPENED",
67        "STATE_CLOSED",
68        "STATE_DISCONNECTED",
69        "STATE_ERROR"
70    };
71
72    /**
73     * Device has not reported any state yet
74     */
75    public static final int STATE_UNINITIALIZED = -1;
76
77    /**
78     * Device is in the first-opened state (transitory)
79     */
80    public static final int STATE_OPENED = 0;
81
82    /**
83     * Device is closed
84     */
85    public static final int STATE_CLOSED = 1;
86
87    /**
88     * Device is disconnected
89     */
90    public static final int STATE_DISCONNECTED = 2;
91
92    /**
93     * Device has encountered a fatal error
94     */
95    public static final int STATE_ERROR = 3;
96
97    /**
98     * Total number of reachable states
99     */
100    private static final int NUM_STATES = 4;
101
102    public BlockingStateCallback() {
103        mProxy = null;
104    }
105
106    public BlockingStateCallback(CameraDevice.StateCallback listener) {
107        mProxy = listener;
108    }
109
110    @Override
111    public void onOpened(CameraDevice camera) {
112        if (mProxy != null) {
113            mProxy.onOpened(camera);
114        }
115        setCurrentState(STATE_OPENED);
116    }
117
118    @Override
119    public void onDisconnected(CameraDevice camera) {
120        if (mProxy != null) {
121            mProxy.onDisconnected(camera);
122        }
123        setCurrentState(STATE_DISCONNECTED);
124    }
125
126    @Override
127    public void onError(CameraDevice camera, int error) {
128        if (mProxy != null) {
129            mProxy.onError(camera, error);
130        }
131        setCurrentState(STATE_ERROR);
132    }
133
134    @Override
135    public void onClosed(CameraDevice camera) {
136        if (mProxy != null) {
137            mProxy.onClosed(camera);
138        }
139        setCurrentState(STATE_CLOSED);
140    }
141
142    /**
143     * Wait until the desired state is observed, checking all state
144     * transitions since the last state that was waited on.
145     *
146     * <p>Note: Only one waiter allowed at a time!</p>
147     *
148     * @param state state to observe a transition to
149     * @param timeout how long to wait in milliseconds
150     *
151     * @throws TimeoutRuntimeException if the desired state is not observed before timeout.
152     */
153    public void waitForState(int state, long timeout) {
154        Integer[] stateArray = { state };
155
156        waitForAnyOfStates(Arrays.asList(stateArray), timeout);
157    }
158
159    /**
160     * Wait until the one of the desired states is observed, checking all
161     * state transitions since the last state that was waited on.
162     *
163     * <p>Note: Only one waiter allowed at a time!</p>
164     *
165     * @param states Set of desired states to observe a transition to.
166     * @param timeout how long to wait in milliseconds
167     *
168     * @return the state reached
169     * @throws TimeoutRuntimeException if none of the states is observed before timeout.
170     *
171     */
172    public int waitForAnyOfStates(Collection<Integer> states, final long timeout) {
173        synchronized (mLock) {
174            if (mWaiting) {
175                throw new IllegalStateException("Only one waiter allowed at a time");
176            }
177            mWaiting = true;
178        }
179        if (VERBOSE) {
180            StringBuilder s = new StringBuilder("Waiting for state(s) ");
181            appendStates(s, states);
182            Log.v(TAG, s.toString());
183        }
184
185        Integer nextState = null;
186        long timeoutLeft = timeout;
187        long startMs = SystemClock.elapsedRealtime();
188        try {
189            while ((nextState = mRecentStates.poll(timeoutLeft, TimeUnit.MILLISECONDS))
190                    != null) {
191                if (VERBOSE) {
192                    Log.v(TAG, "  Saw transition to " + stateToString(nextState));
193                }
194                if (states.contains(nextState)) break;
195                long endMs = SystemClock.elapsedRealtime();
196                timeoutLeft -= (endMs - startMs);
197                startMs = endMs;
198            }
199        } catch (InterruptedException e) {
200            throw new UnsupportedOperationException("Does not support interrupts on waits", e);
201        }
202
203        synchronized (mLock) {
204            mWaiting = false;
205        }
206
207        if (!states.contains(nextState)) {
208            StringBuilder s = new StringBuilder("Timed out after ");
209            s.append(timeout);
210            s.append(" ms waiting for state(s) ");
211            appendStates(s, states);
212
213            throw new TimeoutRuntimeException(s.toString());
214        }
215
216        return nextState;
217    }
218
219    /**
220     * Convert state integer to a String
221     */
222    public static String stateToString(int state) {
223        return mStateNames[state + 1];
224    }
225
226    /**
227     * Append all states to string
228     */
229    public static void appendStates(StringBuilder s, Collection<Integer> states) {
230        boolean start = true;
231        for (Integer state : states) {
232            if (!start) s.append(" ");
233            s.append(stateToString(state));
234            start = false;
235        }
236    }
237}
238