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