/* * Copyright 2013 Robotoworks Limited * 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.robotoworks.mechanoid.ops; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; import android.util.Log; public abstract class OperationProcessor { protected static final int MSG_OPERATION_STARTING = 1; protected static final int MSG_OPERATION_COMPLETE = 2; protected static final int MSG_OPERATION_PROGRESS = 3; protected static final int MSG_OPERATION_ABORTED = 4; protected static final int MSG_WORKER_READY = 5; private LinkedList<Intent> requestQueue = new LinkedList<Intent>(); private Worker mWorker; private boolean mWorkerReady; private final OperationService mService; private Operation mCurrentOperation; private Intent mCurrentRequest; protected final String mLogTag; protected final boolean mEnableLogging; private OperationContext mContext; private Handler handler = new Handler() { public void handleMessage(android.os.Message msg) { switch(msg.what) { case MSG_WORKER_READY: mWorkerReady = true; executePendingOperations(); break; case MSG_OPERATION_STARTING: { if(mEnableLogging) { Log.d(mLogTag, String.format("[Handle Operation Starting] intent:%s", mCurrentRequest)); } mService.onOperationStarting(mCurrentRequest, msg.getData()); break; } case MSG_OPERATION_COMPLETE: { if(mEnableLogging) { Log.d(mLogTag, String.format("[Handle Operation Complete] intent:%s", mCurrentRequest)); } mCurrentOperation = null; mService.onOperationComplete(mCurrentRequest, msg.getData()); executePendingOperations(); break; } case MSG_OPERATION_ABORTED: { if(mEnableLogging) { Log.d(mLogTag, String.format("[Handle Operation Aborted] intent:%s", mCurrentRequest)); } mCurrentOperation = null; mService.onOperationAborted(mCurrentRequest, msg.arg1, msg.getData()); executePendingOperations(); break; } case MSG_OPERATION_PROGRESS: { if(mEnableLogging) { Log.d(mLogTag, String.format("[Handle Operation Progress] intent:%s", mCurrentRequest)); } mService.onOperationProgress(mCurrentRequest, msg.arg1, msg.getData()); break; } } }; }; protected void notifyProgress(int progress, Bundle data) { Message m = handler.obtainMessage(OperationProcessor.MSG_OPERATION_PROGRESS); m.arg1 = progress; m.setData(data); handler.sendMessage(m); } public boolean hasPendingOperations() { return requestQueue.size() > 0; } public OperationProcessor(OperationService service, boolean enableLogging) { this.mService = service; mLogTag = this.getClass().getSimpleName(); mEnableLogging = enableLogging; mContext = new OperationContext(); mWorker = new Worker(handler); mWorker.start(); } public void execute(Intent intent) { if(mEnableLogging) { Log.d(mLogTag, String.format("[Execute (Queue)] intent:%s", intent)); } String action = intent.getAction(); if(action.equals(OperationService.ACTION_ABORT)) { int abortRequestId = intent.getIntExtra(OperationService.EXTRA_REQUEST_ID, 0); int abortReason = intent.getIntExtra(OperationService.EXTRA_ABORT_REASON, 0); abortOperation(abortRequestId, abortReason); return; } // queue this one up requestQueue.offer(intent); executePendingOperations(); } private void abortOperation(int abortRequestId, int abortReason) { if(mEnableLogging) { Log.d(mLogTag, String.format("[Aborting] id:%s, reason:%s", abortRequestId, abortReason)); } // Try to abort if its the current operation if(mCurrentOperation != null) { int currentRequestId = OperationServiceBridge.getOperationRequestId(mCurrentRequest); if(currentRequestId == abortRequestId) { Message m = mContext.handler.obtainMessage(OperationContext.MSG_ABORT, abortReason, 0); mContext.handler.sendMessage(m); return; } } // If its not the current operation then try to find the operation // and flag it as aborted tryFlagQueuedOperationAsAborted(abortRequestId, abortReason); } private void tryFlagQueuedOperationAsAborted(int abortRequestId, int abortReason) { for(int i=0; i < requestQueue.size(); i++) { Intent queuedRequest = requestQueue.get(i); if(OperationServiceBridge.getOperationRequestId(queuedRequest) == abortRequestId) { queuedRequest.putExtra(OperationService.EXTRA_IS_ABORTED, true); queuedRequest.putExtra(OperationService.EXTRA_ABORT_REASON, abortReason); break; } } } private void executePendingOperations() { if(!mWorkerReady) { Log.d(mLogTag, "[Waiting on Worker]"); return; } if(mCurrentOperation != null) { return; } if(mEnableLogging) { Log.d(mLogTag, "[Executing Pending]"); } // pop a request Intent request = requestQueue.poll(); if(request != null) { executeOperation(request); } } public void quit() { if(mEnableLogging) { Log.d(mLogTag, "[Quit]"); } Intent request = null; while((request = requestQueue.poll()) != null) { OperationResult result = OperationResult.error(new OperationServiceStoppedException()); mService.onOperationComplete(request, result.toBundle()); } mWorker.quit(); } private void executeOperation(final Intent request) { if(mEnableLogging) { Log.d(mLogTag, String.format("[Execute Operation] intent:%s", request)); } // // Do not execute this operation if its been marked as aborted // if(request.getBooleanExtra(OperationService.EXTRA_IS_ABORTED, false)) { int abortReason = request.getIntExtra(OperationService.EXTRA_ABORT_REASON, 0); mService.onOperationAborted(request, abortReason, new Bundle()); executePendingOperations(); return; } mCurrentRequest = request; if(request.getAction().equals(OperationService.ACTION_BATCH)) { mCurrentOperation = new BatchOperation(createOperationsFromBatch(request)); } else { mCurrentOperation = createOperation(request.getAction()); } if(mCurrentOperation == null) { throw new RuntimeException(request.getAction() + " Not Implemented"); } mContext.reset(); mContext.setApplicationContext(mService.getApplicationContext()); mContext.setIntent(request); mContext.setOperationProcessor(this); mContext.setEnableLogging(mEnableLogging); mContext.setLogTag(mLogTag); mWorker.post(new OperationRunnable(handler, mContext, mCurrentOperation, mEnableLogging, mLogTag)); } private List<Operation> createOperationsFromBatch(Intent request) { ArrayList<Operation> operations = new ArrayList<Operation>(); ArrayList<Intent> requests = request.getParcelableArrayListExtra(OperationService.EXTRA_BATCH); for(Intent innerRequest : requests) { operations.add(createOperation(innerRequest.getAction())); } return operations; } protected abstract Operation createOperation(String action); static class OperationRunnable implements Runnable { private Operation mOperation; private Handler mCallbackHandler; private boolean mEnableLogging; private String mLogTag; private OperationContext mOperationContext; public OperationRunnable(Handler callbackHandler, OperationContext operationContext, Operation operation, boolean enableLogging, String logTag) { mCallbackHandler = callbackHandler; mOperation = operation; mEnableLogging = enableLogging; mOperationContext = operationContext; mLogTag = logTag; } @Override public void run() { Message messageStarting = mCallbackHandler.obtainMessage(MSG_OPERATION_STARTING); mCallbackHandler.sendMessage(messageStarting); Bundle result = null; try { OperationResult opResult = mOperation.execute(mOperationContext); if(opResult == null) { throw new NullPointerException("OperationResult should not be null"); } result = opResult.toBundle(); } catch(Exception x) { result = OperationResult.error(x).toBundle(); if(mEnableLogging) { Log.e(mLogTag, String.format("[Operation Error] %s", Log.getStackTraceString(x))); } } Message m = null; if(mOperationContext.isAborted()) { m = mCallbackHandler.obtainMessage(MSG_OPERATION_ABORTED); m.arg1 = mOperationContext.getAbortReason(); } else { m = mCallbackHandler.obtainMessage(MSG_OPERATION_COMPLETE); } m.setData(result); mCallbackHandler.sendMessage(m); } } static class Worker extends HandlerThread { private Handler mWorkerHandler; private Handler mProcessorHandler; public Worker(Handler processorHandler) { super("worker", Process.THREAD_PRIORITY_BACKGROUND); mProcessorHandler = processorHandler; } public void post(Runnable runnable) { mWorkerHandler.post(runnable); } @Override protected void onLooperPrepared() { super.onLooperPrepared(); this.mWorkerHandler = new Handler(); mProcessorHandler.sendEmptyMessage(MSG_WORKER_READY); } } }