package com.robotoworks.mechanoid.ops; import java.lang.ref.WeakReference; import android.content.Intent; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; /** * <p>A convenient helper to execute and persist the state of an operation.</p> */ public class OperationExecutor { /** * <p>Execute the operation once only.</p> * <p>In this mode, the operation will only be executed if it has not been executed * before by this executor, to force the operation to execute again, use {@link #MODE_ALWAYS}</p> */ public static final int MODE_ONCE = 0; /** * <p>Always execute this operation regardless of whether it is currently executing, finished, or * has never been executed at all. Currently executing or completed operations will be abandoned but not aborted by * this executor.</p> */ public static final int MODE_ALWAYS = 1; /** * <p>Execute the operation only if it has previously completed with an error or if it has never * been executed before.</p> */ public static final int MODE_ON_ERROR = 2; private static final String TAG = OperationExecutor.class.getSimpleName(); private static final String STATE_KEY = "com.robotoworks.mechanoid.ops.OperationExecutor.State"; private String mUserStateKey; private WeakReference<OperationExecutorCallbacks> mCallbacksRef; private boolean mEnableLogging; private OpInfo mOpInfo; public String getKey() { return mUserStateKey; } /** * @param key The key used to persist this executor through saved state * @param savedInstanceState The state Bundle to persist the state of this executor to * @param callbacks callbacks that will be invoked during the execution of an operation */ public OperationExecutor(String key, Bundle savedInstanceState, OperationExecutorCallbacks callbacks) { this(key, savedInstanceState, callbacks, false); } /** * @param key The key used to persist this executor through saved state * @param savedInstanceState The state Bundle to persist the state of this executor to * @param callbacks callbacks that will be invoked during the execution of an operation * @param enableLogging enable log output for the executor */ public OperationExecutor(String key, Bundle savedInstanceState, OperationExecutorCallbacks callbacks, boolean enableLogging) { mUserStateKey = key; mCallbacksRef = new WeakReference<OperationExecutorCallbacks>(callbacks); mEnableLogging = enableLogging; restoreState(savedInstanceState); Ops.bindListener(mServiceListener); ensureCallbacks(); } /** * Reset this operation */ public void reset() { mOpInfo = null; } /** * Whether the operation is complete. An operation is considered * complete when the completion callback has been successfully invoked. * * @return */ public boolean isComplete() { return (mOpInfo != null && mOpInfo.mCallbackInvoked); } /** * <p>Useful if you want to know if the operation completed and is ok.</p> * * <p>Equivalent to <code>isComplete() && getResult() != null && getResult().isOk()</code></p> * * @return true if the operation completed ok */ public boolean isOk() { return isComplete() && getResult() != null && getResult().isOk(); } /** * <p>Useful if you want to know if an operation completed but with error.</p> * * <p>Equivalent to <code>isComplete() && !getResult().isOk()</code></p> * @return true if the operation completed with error */ public boolean isError() { return isComplete() && !getResult().isOk(); } /** * Whether the operation is currently pending completion. An operation * is considered pending completion when it is currently executing or * waiting to be executed yet a result has not yet been received. * * @return true if the operation has been executed but not yet received a result */ public boolean isPending() { return (mOpInfo != null && mOpInfo.mResult == null); } /** * @return the result of this executors operation, or null if an operation * is yet to be executed */ public OperationResult getResult() { if(mOpInfo == null) { return null; } return mOpInfo.mResult; } /** * @return The intent that represents the operation to execute, set by {@link #execute(Intent, boolean)}. */ public Intent getIntent() { if(mOpInfo == null) { return null; } return mOpInfo.getIntent(0); } private void restoreState(Bundle savedInstanceState) { if(savedInstanceState != null) { Bundle state = savedInstanceState.getBundle(STATE_KEY); if(state != null) { state.setClassLoader(OpInfo.class.getClassLoader()); mOpInfo = state.getParcelable(mUserStateKey); if(mEnableLogging) { Log.d(TAG, String.format("[Restoring State] key:%s", mUserStateKey)); } } } } /** * <p>Saves the state of this operation executor</p> * * @param outState The bundle to save the state to */ public void saveState(Bundle outState) { if(mEnableLogging) { Log.d(TAG, String.format("[Saving State] key: %s", mUserStateKey)); } Bundle state = getStateBundle(outState); state.putParcelable(mUserStateKey, mOpInfo); outState.putBundle(STATE_KEY, state); } private Bundle getStateBundle(Bundle outState) { Bundle state = outState.getBundle(STATE_KEY); if(state == null) { state = new Bundle(); } return state; } private OperationServiceListener mServiceListener = new OperationServiceListener() { @Override public void onOperationComplete(int id, OperationResult result) { if(mOpInfo == null || mOpInfo.mId != id || result == null) { return; } mOpInfo.mResult = result; if(invokeOnOperationComplete(mOpInfo)) { mOpInfo.mCallbackInvoked = true; if(mEnableLogging) { Log.d(TAG, String.format("[Operation Complete] key: %s", mUserStateKey)); } } } }; /** * Remove the callback associated to this executor, this can be useful * in scenarios where you want to ensure that a callback is not received * undesirably such as when a fragment is no longer valid */ public void removeCallback() { mCallbacksRef = null; } public void setCallback(OperationExecutorCallbacks callbacks) { mCallbacksRef = new WeakReference<OperationExecutorCallbacks>(callbacks); } private void ensureCallbacks() { if(mOpInfo == null) { return; } if(mOpInfo.mResult != null) { completeOperation(); return; } if(Ops.isOperationPending(mOpInfo.mId)) { if(mEnableLogging) { Log.d(TAG, String.format("[Operation Pending] request id: %s, key: %s", mOpInfo.mId, mUserStateKey)); } invokeOnOperationPending(); return; } OperationResult result = Ops.getLog().get(mOpInfo.mId); if (result == null) { Log.d(TAG, String.format("[Operation Retry] the log did not contain request id: %s, key: %s, retrying...", mOpInfo.mId, mUserStateKey)); executeOperations(mOpInfo.mIntents); return; } mOpInfo.mResult = result; completeOperation(); } static class OpInfo implements Parcelable { int mId = 0; boolean mCallbackInvoked = false; OperationResult mResult = null; public Intent[] mIntents; public static final Parcelable.Creator<OpInfo> CREATOR = new Parcelable.Creator<OpInfo>() { public OpInfo createFromParcel(Parcel in) { return new OpInfo(in); } public OpInfo[] newArray(int size) { return new OpInfo[size]; } }; OpInfo(Intent[] intents) { mIntents = intents; } public Intent getIntent(int index) { if(mIntents == null || mIntents.length == 0) { return null; } return mIntents[index]; } OpInfo(Parcel in) { mId = in.readInt(); mCallbackInvoked = in.readInt() > 0; mResult = in.readParcelable(OperationResult.class.getClassLoader()); int numInBatch = in.readInt(); mIntents = new Intent[numInBatch]; in.readTypedArray(mIntents, Intent.CREATOR); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mId); dest.writeInt(mCallbackInvoked ? 1 : 0); dest.writeParcelable(mResult, 0); dest.writeInt(mIntents.length); dest.writeTypedArray(mIntents, 0); } } protected boolean invokeOnOperationPending() { if(mCallbacksRef == null) { return false; } OperationExecutorCallbacks callbacks = mCallbacksRef.get(); if(callbacks == null) { return false; } callbacks.onOperationPending(mUserStateKey); return true; } protected boolean invokeOnOperationComplete(OpInfo info) { try { if(info == null || mCallbacksRef == null) { return false; } OperationResult result = info.mResult; OperationExecutorCallbacks callbacks = mCallbacksRef.get(); if(result == null || callbacks == null) { return false; } return callbacks.onOperationComplete(mUserStateKey, result); } catch(Exception x) { throw new RuntimeException(this.getKey() + " unhandled exception", x); } } /** * <p>Execute an operation</p> * * <p>When the operation completes, {@link OperationExecutorCallbacks#onOperationComplete(OperationExecutor, OperationResult, boolean)} * will be invoked.</p> * <p><b>execute</b> can be invoked many times for the same operation intent, however it will only run the operation once if * the <b>force</b> flag is set to false, * subsequent calls will be ignored unless the <b>force</b> argument is set to true. In all cases the * {@link OperationExecutorCallbacks#onOperationComplete(OperationExecutor, OperationResult, boolean)} will * be invoked for each call but subsequent calls for the same operation will have the <b>fromCache</b> argument set to true.</p> * * <p>Setting the force flag to true, will force the operation to run, if an operation is currently running then it will continue to run but * its result will be ignored and no callbacks will be received.</p> * * @param operationIntent An intent representing the operation to execute * @param force true to force the operation intent to execute, this will abandon any previous operation * intent */ @Deprecated public void execute(Intent operationIntent, int mode) { if (operationIntent == null) { Log.d(TAG, String.format("[Operation Null] operationintent argument was null, key: %s", mUserStateKey)); return; } if (mode == MODE_ALWAYS) { mOpInfo = null; executeOperations(operationIntent); } else if(mode == MODE_ONCE) { if(mOpInfo == null) { executeOperations(operationIntent); } } else if(mode == MODE_ON_ERROR) { if(mOpInfo == null || isError()) { executeOperations(operationIntent); } } completeOperation(); } /** * <p>Execute operation(s)</p> * * <p>When the operation completes, {@link OperationExecutorCallbacks#onOperationComplete(OperationExecutor, OperationResult, boolean)} * will be invoked.</p> * <p><b>execute</b> can be invoked many times for the same operation intent, however it will only run the operation once if * the <b>force</b> flag is set to false, * subsequent calls will be ignored unless the <b>force</b> argument is set to true. In all cases the * {@link OperationExecutorCallbacks#onOperationComplete(OperationExecutor, OperationResult, boolean)} will * be invoked for each call but subsequent calls for the same operation will have the <b>fromCache</b> argument set to true.</p> * * <p>Setting the force flag to true, will force the operation to run, if an operation is currently running then it will continue to run but * its result will be ignored and no callbacks will be received.</p> * * @param mode true to force the operation intent to execute, this will abandon any previous operation * @param intents Intent(s) representing operation(s) to execute * intent */ public void execute(int mode, Intent... intents) { if (intents == null) { Log.d(TAG, String.format("[Operation Null] operationintent argument was null, key: %s", mUserStateKey)); return; } if (mode == MODE_ALWAYS) { mOpInfo = null; executeOperations(intents); } else if(mode == MODE_ONCE) { if(mOpInfo == null) { executeOperations(intents); } } else if(mode == MODE_ON_ERROR) { if(mOpInfo == null || isError()) { executeOperations(intents); } } completeOperation(); } private void completeOperation() { if(mOpInfo.mResult != null && !mOpInfo.mCallbackInvoked) { if(invokeOnOperationComplete(mOpInfo)) { if(mEnableLogging) { Log.d(TAG, String.format("[Operation Complete] request id: %s, key: %s", mOpInfo.mId, mUserStateKey)); } mOpInfo.mCallbackInvoked = true; } } } protected void executeOperations(Intent... operations) { if(mEnableLogging) { Log.d(TAG, String.format("[Execute Operation] key: %s", mUserStateKey)); } mOpInfo = new OpInfo(operations); invokeOnOperationPending(); if(operations.length == 1) { mOpInfo.mId = Ops.execute(operations[0]); } else { mOpInfo.mId = Ops.executeBatch(operations); } } }