parent
fd17a8339e
commit
57a4cfba80
@ -1,178 +0,0 @@ |
|||||||
package com.otaliastudios.cameraview.engine; |
|
||||||
|
|
||||||
import com.google.android.gms.tasks.Continuation; |
|
||||||
import com.google.android.gms.tasks.OnFailureListener; |
|
||||||
import com.google.android.gms.tasks.SuccessContinuation; |
|
||||||
import com.google.android.gms.tasks.Task; |
|
||||||
import com.google.android.gms.tasks.Tasks; |
|
||||||
import com.otaliastudios.cameraview.CameraLogger; |
|
||||||
|
|
||||||
import java.util.concurrent.Callable; |
|
||||||
import java.util.concurrent.Executor; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.annotation.Nullable; |
|
||||||
import androidx.annotation.VisibleForTesting; |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents one of the steps in the {@link CameraEngine} setup: for example, the engine step, |
|
||||||
* the bind-to-surface step, and the preview step. |
|
||||||
* |
|
||||||
* A step is something that can be setup (started) or torn down (stopped), and |
|
||||||
* steps can of course depend onto each other. |
|
||||||
* |
|
||||||
* The purpose of this class is to manage the step state (stopping, stopped, starting or started) |
|
||||||
* and, more importantly, to perform START and STOP operations in such a way that they do not |
|
||||||
* overlap. For example, if we're stopping, we're wait for stop to finish before starting again. |
|
||||||
* |
|
||||||
* This is an important condition for simplifying the engine code. |
|
||||||
* Since Camera1, the only requirement was basically to use a single thread. |
|
||||||
* Since Camera2, which has an asynchronous API, further care must be used. |
|
||||||
* |
|
||||||
* For this reason, we use Google's {@link Task} abstraction and only start new operations |
|
||||||
* once the previous one has ended. |
|
||||||
* |
|
||||||
* <strong>This class is NOT thread safe!</string> |
|
||||||
*/ |
|
||||||
class Step { |
|
||||||
|
|
||||||
private static final String TAG = Step.class.getSimpleName(); |
|
||||||
private static final CameraLogger LOG = CameraLogger.create(TAG); |
|
||||||
|
|
||||||
interface Callback { |
|
||||||
@NonNull |
|
||||||
Executor getExecutor(); |
|
||||||
void handleException(@NonNull Exception exception); |
|
||||||
} |
|
||||||
|
|
||||||
static final int STATE_STOPPING = -1; |
|
||||||
static final int STATE_STOPPED = 0; |
|
||||||
static final int STATE_STARTING = 1; |
|
||||||
static final int STATE_STARTED = 2; |
|
||||||
|
|
||||||
private int state = STATE_STOPPED; |
|
||||||
|
|
||||||
// To avoid dirty scenarios (e.g. calling stopXXX while XXX is starting),
|
|
||||||
// and since every operation can be asynchronous, we use some tasks for each step.
|
|
||||||
private Task<Void> task = Tasks.forResult(null); |
|
||||||
|
|
||||||
private final String name; |
|
||||||
private final Callback callback; |
|
||||||
|
|
||||||
Step(@NonNull String name, @NonNull Callback callback) { |
|
||||||
this.name = name.toUpperCase(); |
|
||||||
this.callback = callback; |
|
||||||
} |
|
||||||
|
|
||||||
int getState() { |
|
||||||
return state; |
|
||||||
} |
|
||||||
|
|
||||||
@VisibleForTesting void setState(int newState) { |
|
||||||
state = newState; |
|
||||||
} |
|
||||||
|
|
||||||
@NonNull |
|
||||||
String getStateName() { |
|
||||||
switch (state) { |
|
||||||
case STATE_STOPPING: return name + "_STATE_STOPPING"; |
|
||||||
case STATE_STOPPED: return name + "_STATE_STOPPED"; |
|
||||||
case STATE_STARTING: return name + "_STATE_STARTING"; |
|
||||||
case STATE_STARTED: return name + "_STATE_STARTED"; |
|
||||||
} |
|
||||||
return "null"; |
|
||||||
} |
|
||||||
|
|
||||||
boolean isStoppingOrStopped() { |
|
||||||
return state == STATE_STOPPING || state == STATE_STOPPED; |
|
||||||
} |
|
||||||
|
|
||||||
boolean isStartedOrStarting() { |
|
||||||
return state == STATE_STARTING || state == STATE_STARTED; |
|
||||||
} |
|
||||||
|
|
||||||
boolean isStarted() { |
|
||||||
return state == STATE_STARTED; |
|
||||||
} |
|
||||||
|
|
||||||
@NonNull |
|
||||||
Task<Void> getTask() { |
|
||||||
return task; |
|
||||||
} |
|
||||||
|
|
||||||
@SuppressWarnings({"SameParameterValue", "UnusedReturnValue"}) |
|
||||||
Task<Void> doStart(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) { |
|
||||||
return doStart(swallowExceptions, op, null); |
|
||||||
} |
|
||||||
|
|
||||||
Task<Void> doStart(final boolean swallowExceptions, |
|
||||||
final @NonNull Callable<Task<Void>> op, |
|
||||||
final @Nullable Runnable onStarted) { |
|
||||||
LOG.i(name, "doStart", "Called. Enqueuing."); |
|
||||||
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() { |
|
||||||
@Override |
|
||||||
public Task<Void> then(@NonNull Task<Void> task) throws Exception { |
|
||||||
LOG.i(name, "doStart", "About to start. Setting state to STARTING"); |
|
||||||
setState(STATE_STARTING); |
|
||||||
return op.call().addOnFailureListener(callback.getExecutor(), |
|
||||||
new OnFailureListener() { |
|
||||||
@Override |
|
||||||
public void onFailure(@NonNull Exception e) { |
|
||||||
LOG.w(name, "doStart", "Failed with error", e, |
|
||||||
"Setting state to STOPPED"); |
|
||||||
setState(STATE_STOPPED); |
|
||||||
if (!swallowExceptions) callback.handleException(e); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() { |
|
||||||
@NonNull |
|
||||||
@Override |
|
||||||
public Task<Void> then(@Nullable Void aVoid) { |
|
||||||
LOG.i(name, "doStart", "Succeeded! Setting state to STARTED"); |
|
||||||
setState(STATE_STARTED); |
|
||||||
if (onStarted != null) onStarted.run(); |
|
||||||
return Tasks.forResult(null); |
|
||||||
} |
|
||||||
}); |
|
||||||
return task; |
|
||||||
} |
|
||||||
|
|
||||||
@SuppressWarnings("UnusedReturnValue") |
|
||||||
Task<Void> doStop(final boolean swallowExceptions, final @NonNull Callable<Task<Void>> op) { |
|
||||||
return doStop(swallowExceptions, op, null); |
|
||||||
} |
|
||||||
|
|
||||||
Task<Void> doStop(final boolean swallowExceptions, |
|
||||||
final @NonNull Callable<Task<Void>> op, |
|
||||||
final @Nullable Runnable onStopped) { |
|
||||||
LOG.i(name, "doStop", "Called. Enqueuing."); |
|
||||||
task = task.continueWithTask(callback.getExecutor(), new Continuation<Void, Task<Void>>() { |
|
||||||
@Override |
|
||||||
public Task<Void> then(@NonNull Task<Void> task) throws Exception { |
|
||||||
LOG.i(name, "doStop", "About to stop. Setting state to STOPPING"); |
|
||||||
state = STATE_STOPPING; |
|
||||||
return op.call().addOnFailureListener(callback.getExecutor(), |
|
||||||
new OnFailureListener() { |
|
||||||
@Override |
|
||||||
public void onFailure(@NonNull Exception e) { |
|
||||||
LOG.w(name, "doStop", "Failed with error", e, |
|
||||||
"Setting state to STOPPED"); |
|
||||||
state = STATE_STOPPED; |
|
||||||
if (!swallowExceptions) callback.handleException(e); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}).onSuccessTask(callback.getExecutor(), new SuccessContinuation<Void, Void>() { |
|
||||||
@NonNull |
|
||||||
@Override |
|
||||||
public Task<Void> then(@Nullable Void aVoid) { |
|
||||||
LOG.i(name, "doStop", "Succeeded! Setting state to STOPPED"); |
|
||||||
state = STATE_STOPPED; |
|
||||||
if (onStopped != null) onStopped.run(); |
|
||||||
return Tasks.forResult(null); |
|
||||||
} |
|
||||||
}); |
|
||||||
return task; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,180 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine.orchestrator; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.Nullable; |
||||||
|
|
||||||
|
import com.google.android.gms.tasks.OnCompleteListener; |
||||||
|
import com.google.android.gms.tasks.Task; |
||||||
|
import com.google.android.gms.tasks.TaskCompletionSource; |
||||||
|
import com.google.android.gms.tasks.Tasks; |
||||||
|
import com.otaliastudios.cameraview.CameraLogger; |
||||||
|
import com.otaliastudios.cameraview.internal.utils.WorkerHandler; |
||||||
|
|
||||||
|
import java.util.ArrayDeque; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.Callable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Schedules {@link com.otaliastudios.cameraview.engine.CameraEngine} actions, |
||||||
|
* so that they always run on the same thread. |
||||||
|
* |
||||||
|
* We need to be extra careful (not as easy as posting on a Handler) because the engine |
||||||
|
* has different states, and some actions will modify the engine state - turn it on or |
||||||
|
* tear it down. Other actions might need a specific state to be executed. |
||||||
|
* And most importantly, some actions will finish asynchronously, so subsequent actions |
||||||
|
* should wait for the previous to finish, but without blocking the thread. |
||||||
|
*/ |
||||||
|
@SuppressWarnings("WeakerAccess") |
||||||
|
public class CameraOrchestrator { |
||||||
|
|
||||||
|
protected static final String TAG = CameraOrchestrator.class.getSimpleName(); |
||||||
|
protected static final CameraLogger LOG = CameraLogger.create(TAG); |
||||||
|
|
||||||
|
public interface Callback { |
||||||
|
@NonNull |
||||||
|
WorkerHandler getJobWorker(@NonNull String job); |
||||||
|
void handleJobException(@NonNull String job, @NonNull Exception exception); |
||||||
|
} |
||||||
|
|
||||||
|
protected static class Token { |
||||||
|
public final String name; |
||||||
|
public final Task<Void> task; |
||||||
|
|
||||||
|
private Token(@NonNull String name, @NonNull Task<Void> task) { |
||||||
|
this.name = name; |
||||||
|
this.task = task; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean equals(@Nullable Object obj) { |
||||||
|
return obj instanceof Token && ((Token) obj).name.equals(name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected final Callback mCallback; |
||||||
|
protected final ArrayDeque<Token> mJobs = new ArrayDeque<>(); |
||||||
|
protected final Object mLock = new Object(); |
||||||
|
private final Map<String, Runnable> mDelayedJobs = new HashMap<>(); |
||||||
|
|
||||||
|
public CameraOrchestrator(@NonNull Callback callback) { |
||||||
|
mCallback = callback; |
||||||
|
ensureToken(); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public Task<Void> schedule(@NonNull String name, |
||||||
|
boolean dispatchExceptions, |
||||||
|
@NonNull final Runnable job) { |
||||||
|
return schedule(name, dispatchExceptions, new Callable<Task<Void>>() { |
||||||
|
@Override |
||||||
|
public Task<Void> call() { |
||||||
|
job.run(); |
||||||
|
return Tasks.forResult(null); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public Task<Void> schedule(@NonNull final String name, |
||||||
|
final boolean dispatchExceptions, |
||||||
|
@NonNull final Callable<Task<Void>> job) { |
||||||
|
LOG.i(name.toUpperCase(), "- Scheduling."); |
||||||
|
final TaskCompletionSource<Void> source = new TaskCompletionSource<>(); |
||||||
|
final WorkerHandler handler = mCallback.getJobWorker(name); |
||||||
|
synchronized (mLock) { |
||||||
|
applyCompletionListener(mJobs.getLast().task, handler, |
||||||
|
new OnCompleteListener<Void>() { |
||||||
|
@Override |
||||||
|
public void onComplete(@NonNull Task<Void> task) { |
||||||
|
synchronized (mLock) { |
||||||
|
mJobs.removeFirst(); |
||||||
|
ensureToken(); |
||||||
|
} |
||||||
|
try { |
||||||
|
LOG.i(name.toUpperCase(), "- Executing."); |
||||||
|
Task<Void> inner = job.call(); |
||||||
|
applyCompletionListener(inner, handler, new OnCompleteListener<Void>() { |
||||||
|
@Override |
||||||
|
public void onComplete(@NonNull Task<Void> task) { |
||||||
|
Exception e = task.getException(); |
||||||
|
LOG.i(name.toUpperCase(), "- Finished.", e); |
||||||
|
if (e != null) { |
||||||
|
if (dispatchExceptions) { |
||||||
|
mCallback.handleJobException(name, e); |
||||||
|
} |
||||||
|
source.trySetException(e); |
||||||
|
} else { |
||||||
|
source.trySetResult(null); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} catch (Exception e) { |
||||||
|
LOG.i(name.toUpperCase(), "- Finished.", e); |
||||||
|
if (dispatchExceptions) mCallback.handleJobException(name, e); |
||||||
|
source.trySetException(e); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
mJobs.addLast(new Token(name, source.getTask())); |
||||||
|
} |
||||||
|
return source.getTask(); |
||||||
|
} |
||||||
|
|
||||||
|
public void scheduleDelayed(@NonNull final String name, |
||||||
|
long minDelay, |
||||||
|
@NonNull final Runnable runnable) { |
||||||
|
Runnable wrapper = new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
schedule(name, true, runnable); |
||||||
|
synchronized (mLock) { |
||||||
|
if (mDelayedJobs.containsValue(this)) { |
||||||
|
mDelayedJobs.remove(name); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
synchronized (mLock) { |
||||||
|
mDelayedJobs.put(name, wrapper); |
||||||
|
mCallback.getJobWorker(name).post(minDelay, wrapper); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void remove(@NonNull String name) { |
||||||
|
synchronized (mLock) { |
||||||
|
if (mDelayedJobs.get(name) != null) { |
||||||
|
//noinspection ConstantConditions
|
||||||
|
mCallback.getJobWorker(name).remove(mDelayedJobs.get(name)); |
||||||
|
mDelayedJobs.remove(name); |
||||||
|
} |
||||||
|
Token token = new Token(name, Tasks.<Void>forResult(null)); |
||||||
|
//noinspection StatementWithEmptyBody
|
||||||
|
while (mJobs.remove(token)) { /* do nothing */ } |
||||||
|
ensureToken(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void ensureToken() { |
||||||
|
synchronized (mLock) { |
||||||
|
if (mJobs.isEmpty()) { |
||||||
|
mJobs.add(new Token("BASE", Tasks.<Void>forResult(null))); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void applyCompletionListener(@NonNull final Task<Void> task, |
||||||
|
@NonNull WorkerHandler handler, |
||||||
|
@NonNull final OnCompleteListener<Void> listener) { |
||||||
|
if (task.isComplete()) { |
||||||
|
handler.run(new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
listener.onComplete(task); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
task.addOnCompleteListener(handler.getExecutor(), listener); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine.orchestrator; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
|
||||||
|
public enum CameraState { |
||||||
|
OFF(0), ENGINE(1), BIND(2), PREVIEW(3); |
||||||
|
|
||||||
|
private int mState; |
||||||
|
|
||||||
|
CameraState(int state) { |
||||||
|
mState = state; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isAtLeast(@NonNull CameraState reference) { |
||||||
|
return mState >= reference.mState; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
package com.otaliastudios.cameraview.engine.orchestrator; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
|
||||||
|
import com.google.android.gms.tasks.Continuation; |
||||||
|
import com.google.android.gms.tasks.OnCompleteListener; |
||||||
|
import com.google.android.gms.tasks.Task; |
||||||
|
import com.google.android.gms.tasks.Tasks; |
||||||
|
|
||||||
|
import java.util.concurrent.Callable; |
||||||
|
import java.util.concurrent.Executor; |
||||||
|
|
||||||
|
/** |
||||||
|
* A special {@link CameraOrchestrator} with special methods that deal with the |
||||||
|
* {@link CameraState}. |
||||||
|
*/ |
||||||
|
public class CameraStateOrchestrator extends CameraOrchestrator { |
||||||
|
|
||||||
|
private CameraState mCurrentState = CameraState.OFF; |
||||||
|
private CameraState mTargetState = CameraState.OFF; |
||||||
|
private int mStateChangeCount = 0; |
||||||
|
|
||||||
|
public CameraStateOrchestrator(@NonNull Callback callback) { |
||||||
|
super(callback); |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public CameraState getCurrentState() { |
||||||
|
return mCurrentState; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public CameraState getTargetState() { |
||||||
|
return mTargetState; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean hasPendingStateChange() { |
||||||
|
synchronized (mLock) { |
||||||
|
for (Token token : mJobs) { |
||||||
|
if (token.name.contains(" > ") && !token.task.isComplete()) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
public Task<Void> scheduleStateChange(@NonNull final CameraState fromState, |
||||||
|
@NonNull final CameraState toState, |
||||||
|
boolean dispatchExceptions, |
||||||
|
@NonNull final Callable<Task<Void>> stateChange) { |
||||||
|
final int changeCount = ++mStateChangeCount; |
||||||
|
mTargetState = toState; |
||||||
|
|
||||||
|
final boolean isTearDown = !toState.isAtLeast(fromState); |
||||||
|
final String changeName = fromState.name() + " > " + toState.name(); |
||||||
|
return schedule(changeName, dispatchExceptions, new Callable<Task<Void>>() { |
||||||
|
@Override |
||||||
|
public Task<Void> call() throws Exception { |
||||||
|
if (getCurrentState() != fromState) { |
||||||
|
LOG.w(changeName.toUpperCase(), "- State mismatch, aborting. current:", |
||||||
|
getCurrentState(), "from:", fromState, "to:", toState); |
||||||
|
return Tasks.forResult(null); |
||||||
|
} else { |
||||||
|
Executor executor = mCallback.getJobWorker(changeName).getExecutor(); |
||||||
|
return stateChange.call().continueWithTask(executor, |
||||||
|
new Continuation<Void, Task<Void>>() { |
||||||
|
@Override |
||||||
|
public Task<Void> then(@NonNull Task<Void> task) { |
||||||
|
if (task.isSuccessful() || isTearDown) { |
||||||
|
mCurrentState = toState; |
||||||
|
} |
||||||
|
return task; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}).addOnCompleteListener(new OnCompleteListener<Void>() { |
||||||
|
@Override |
||||||
|
public void onComplete(@NonNull Task<Void> task) { |
||||||
|
if (changeCount == mStateChangeCount) { |
||||||
|
mTargetState = mCurrentState; |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("UnusedReturnValue") |
||||||
|
@NonNull |
||||||
|
public Task<Void> scheduleStateful(@NonNull String name, |
||||||
|
@NonNull final CameraState atLeast, |
||||||
|
@NonNull final Runnable job) { |
||||||
|
return schedule(name, true, new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
if (getCurrentState().isAtLeast(atLeast)) { |
||||||
|
job.run(); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public void scheduleStatefulDelayed(@NonNull String name, |
||||||
|
@NonNull final CameraState atLeast, |
||||||
|
long delay, |
||||||
|
@NonNull final Runnable job) { |
||||||
|
scheduleDelayed(name, delay, new Runnable() { |
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
if (getCurrentState().isAtLeast(atLeast)) { |
||||||
|
job.run(); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue