blob: 1eb3037a2a606a0bf273094f43f00eb41141a321 [file] [log] [blame]
/*
* Copyright 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ex.camera2.utils;
import android.os.SystemClock;
import android.util.Log;
import com.android.ex.camera2.exceptions.TimeoutRuntimeException;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Block until a specific state change occurs.
*
* <p>Provides wait calls that block until the next unobserved state of the
* requested type arrives. Unobserved states are states that have occurred since
* the last wait, or that will be received from the camera device in the
* future.</p>
*
* <p>Thread interruptions are not supported; interrupting a thread that is either
* waiting with {@link #waitForState} / {@link #waitForAnyOfStates} or is currently in
* {@link StateChangeListener#onStateChanged} (provided by {@link #getListener}) will result in an
* {@link UnsupportedOperationException} being raised on that thread.</p>
*/
public final class StateWaiter {
private static final String TAG = "StateWaiter";
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
private final String[] mStateNames;
private final int mStateCount;
private final StateChangeListener mListener;
/** Guard waitForState, waitForAnyState to only have one waiter */
private final AtomicBoolean mWaiting = new AtomicBoolean(false);
private final LinkedBlockingQueue<Integer> mQueuedStates = new LinkedBlockingQueue<>();
/**
* Create a new state waiter.
*
* <p>All {@code state}/{@code states} arguments used in other methods must be
* in the range of {@code [0, stateNames.length - 1]}.</p>
*
* @param stateNames an array of string names, used to mark the range of the valid states
*/
public StateWaiter(String[] stateNames) {
mStateCount = stateNames.length;
mStateNames = new String[mStateCount];
System.arraycopy(stateNames, /*srcPos*/0, mStateNames, /*dstPos*/0, mStateCount);
mListener = new StateChangeListener() {
@Override
public void onStateChanged(int state) {
queueStateTransition(checkStateInRange(state));
}
};
}
public StateChangeListener getListener() {
return mListener;
}
/**
* Wait until the desired state is observed, checking all state
* transitions since the last time a state was waited on.
*
* <p>Any intermediate state transitions that is not {@code state} are ignored.</p>
*
* <p>Note: Only one waiter allowed at a time!</p>
*
* @param state state to observe a transition to
* @param timeoutMs how long to wait in milliseconds
*
* @throws IllegalArgumentException if {@code state} was out of range
* @throws TimeoutRuntimeException if the desired state is not observed before timeout.
* @throws IllegalStateException if another thread is already waiting for a state transition
*/
public void waitForState(int state, long timeoutMs) {
Integer[] stateArray = { checkStateInRange(state) };
waitForAnyOfStates(Arrays.asList(stateArray), timeoutMs);
}
/**
* Wait until the one of the desired {@code states} is observed, checking all
* state transitions since the last time a state was waited on.
*
* <p>Any intermediate state transitions that are not in {@code states} are ignored.</p>
*
* <p>Note: Only one waiter allowed at a time!</p>
*
* @param states Set of desired states to observe a transition to.
* @param timeoutMs how long to wait in milliseconds
*
* @return the state reached
*
* @throws IllegalArgumentException if {@code state} was out of range
* @throws TimeoutRuntimeException if none of the states is observed before timeout.
* @throws IllegalStateException if another thread is already waiting for a state transition
*/
public int waitForAnyOfStates(Collection<Integer> states, final long timeoutMs) {
checkStateCollectionInRange(states);
// Acquire exclusive waiting privileges
if (mWaiting.getAndSet(true)) {
throw new IllegalStateException("Only one waiter allowed at a time");
}
Integer nextState = null;
try {
if (VERBOSE) {
StringBuilder s = new StringBuilder("Waiting for state(s) ");
appendStateNames(s, states);
Log.v(TAG, s.toString());
}
long timeoutLeft = timeoutMs;
long startMs = SystemClock.elapsedRealtime();
while ((nextState = mQueuedStates.poll(timeoutLeft, TimeUnit.MILLISECONDS)) != null) {
if (VERBOSE) {
Log.v(TAG, " Saw transition to " + getStateName(nextState));
}
if (states.contains(nextState)) {
break;
}
long endMs = SystemClock.elapsedRealtime();
timeoutLeft -= (endMs - startMs);
startMs = endMs;
}
} catch (InterruptedException e) {
throw new UnsupportedOperationException("Does not support interrupts on waits", e);
} finally {
// Release exclusive waiting privileges
mWaiting.set(false);
}
if (!states.contains(nextState)) {
StringBuilder s = new StringBuilder("Timed out after ");
s.append(timeoutMs);
s.append(" ms waiting for state(s) ");
appendStateNames(s, states);
throw new TimeoutRuntimeException(s.toString());
}
return nextState;
}
/**
* Convert state integer to a String
*/
public String getStateName(int state) {
return mStateNames[checkStateInRange(state)];
}
/**
* Append all states to string
*/
public void appendStateNames(StringBuilder s, Collection<Integer> states) {
checkStateCollectionInRange(states);
boolean start = true;
for (Integer state : states) {
if (!start) {
s.append(" ");
}
s.append(getStateName(state));
start = false;
}
}
private void queueStateTransition(int state) {
if (VERBOSE) Log.v(TAG, "setCurrentState - state now " + getStateName(state));
try {
mQueuedStates.put(state);
} catch (InterruptedException e) {
throw new UnsupportedOperationException("Unable to set current state", e);
}
}
private int checkStateInRange(int state) {
if (state < 0 || state >= mStateCount) {
throw new IllegalArgumentException("State out of range " + state);
}
return state;
}
private Collection<Integer> checkStateCollectionInRange(Collection<Integer> states) {
for (int state : states) {
checkStateInRange(state);
}
return states;
}
}