1

My goal is to have an AsyncTask that

  • can execute multiple times (one task at a time of course)
  • its current task can be cancelled
  • can be used by any activity
  • can execute many different tasks
  • does not have any problem with screen rotation (or phonecalls etc)

To achieve that i have created the classes shown below. But my experience with (and understanding of) threads is very limited. And since i don't know of any way to debug multiple threads, there is no way (for me) of knowing if this is going to work or not. So what i'm really asking is: Is this code ok?

And since there is no code that it is currently using this, here's an example use for it:

Data2Get d2g = new Data2Get(this, Data2Get.OpCountNumbers);
d2g.setParam("up2Num", String.valueOf(800));
LongOpsRunner.getLongOpsRunner().runOp(d2g);

So, here we go. This is the interface that every activity that wants to execute a long task (operation - op) should implement:

public interface LongOpsActivity {
    public void onTaskCompleted(OpResult result);
}

This is a class to enclose any result of any task:

public class OpResult {

    public LongOpsActivity forActivity;
    public int opType;
    public Object result;

    public OpResult(LongOpsActivity forActivity, int opType, Object result){
        this.forActivity = forActivity;
        this.opType = opType;
        this.result = result;
    }
}

And finally the big part, the singleton async task class:

import java.util.HashMap;
import java.util.Map.Entry;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import android.os.AsyncTask;

public class LongOpsRunner extends AsyncTask<Void, OpResult, Void> {

    public class Data2Get implements Cloneable {

        // one id for each operation
        public static final int OpCountNumbers = 1;
        public static final int OpCountLetters = 2;

        public LongOpsActivity forActivity;
        public int opType;
        private HashMap<String, String> params = new HashMap<String, String>();

        public Data2Get(LongOpsActivity forActivity, int opType) {
            this.forActivity = forActivity;
            this.opType = opType;
        }

        public void setParam(String key, String value) {
            params.put(key, value);
        }

        public String getParam(String key) {
            return params.get(key);
        }

        public void clearParams() {
            params.clear();
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            // deep clone
            Data2Get myClone = (Data2Get) super.clone();
            myClone.clearParams();
            for (Entry<String, String> entry : params.entrySet()) {
                myClone.setParam(new String(entry.getKey()), new String(entry.getValue()));
            }
            return myClone;
        }
    }

    private class IntermediateResult extends OpResult {

        public IntermediateResult(LongOpsActivity forActivity, int opType, Object result) {
            super(forActivity, opType, result);
        }
    }

    // not really needed
    private class FinalResult extends OpResult {

        public FinalResult(LongOpsActivity forActivity, int opType, Object result) {
            super(forActivity, opType, result);
        }
    }

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition executeOp = lock.newCondition();
    private volatile boolean finished = false;
    private volatile boolean waiting = true;
    private volatile boolean shouldCancel = false;
    private volatile boolean activityHasBeenNotified = true;
    private Data2Get startingOpParams = null;
    private Data2Get currentOpParams = null;
    private FinalResult currentOpResult;

    protected Void doInBackground(Void... nothing) {

        try {
            lock.lockInterruptibly();

            do {
                waiting = true;
                while (waiting) {
                    executeOp.await();
                }

                shouldCancel = false;
                activityHasBeenNotified = false;
                boolean opCancelled = false;
                try {
                    currentOpParams = (Data2Get) startingOpParams.clone();
                } catch (CloneNotSupportedException cns) {
                    // do nothing
                }

                switch (currentOpParams.opType) {
                case Data2Get.OpCountNumbers:
                    int numberCounter = 0;
                    int numLoopCount = 0;
                    while ((!opCancelled) & (numLoopCount <= 5000000)) {
                        if (!shouldCancel) {
                            numberCounter = (numberCounter + 1)
                                    % Integer.parseInt(currentOpParams.getParam("up2Num"));
                            if (numberCounter == 0) {
                                numLoopCount++;
                                publishProgress(new IntermediateResult(
                                        currentOpParams.forActivity,
                                        currentOpParams.opType,
                                        "Numbers loop count:" + numLoopCount));
                            }
                        } else {
                            opCancelled = true;
                            activityHasBeenNotified = true;
                        }
                        if (!opCancelled) {
                            currentOpResult = new FinalResult(
                                    currentOpParams.forActivity,
                                    currentOpParams.opType,
                                    "Numbers loop completed.");
                            publishProgress(currentOpResult);
                        }
                    }
                    break;
                case Data2Get.OpCountLetters:
                    int letterLoopCount = 0;
                    char ch = 'a';
                    while (!opCancelled & (letterLoopCount <= 5000000)) {
                        if (!shouldCancel) {
                            ch++;
                            if (Character.toString(ch).equals(currentOpParams.getParam("up2Letter"))) {
                                ch = 'a';
                                letterLoopCount++;
                                publishProgress(new IntermediateResult(
                                        currentOpParams.forActivity,
                                        currentOpParams.opType,
                                        "Letters loop count:" + letterLoopCount));
                            }
                        } else {
                            opCancelled = true;
                            activityHasBeenNotified = true;
                        }
                        if (!opCancelled) {
                            currentOpResult = new FinalResult(
                                    currentOpParams.forActivity,
                                    currentOpParams.opType,
                                    "Letters loop completed.");
                            publishProgress(currentOpResult);
                        }
                    }
                    break;
                default:
                }

            } while (!finished);

            lock.unlock();
        } catch (InterruptedException e) {
            // do nothing
        }
        return null;
    }

    public void cancelCurrentOp() {
        shouldCancel = true;
    }

    @Override
    protected void onProgressUpdate(OpResult... res) {
        OpResult result = res[0];
        if (result instanceof IntermediateResult) {
            // normal progress update
            // use result.forActivity to show something in the activity
        } else {
            notifyActivityOpCompleted(result);
        }
    }

    public boolean currentOpIsFinished() {
        return waiting;
    }

    public void runOp(Data2Get d2g) {
        // Call this to run an operation
        // Should check first currentOpIsFinished() most of the times
        startingOpParams = d2g;
        waiting = false;
        executeOp.signal();
    }

    public void terminateAsyncTask() {
        // The task will only finish when we call this method
        finished = true;
        lock.unlock(); // won't this throw an exception?
    }

    protected void onCancelled() {
        // Make sure we clean up if the task is killed
        terminateAsyncTask();
    }

    // if phone is rotated, use setActivity(null) inside
    // onRetainNonConfigurationInstance()
    // and setActivity(this) inside the constructor
    // and all that only if there is an operation still running
    public void setActivity(LongOpsActivity activity) {
        currentOpParams.forActivity = activity;
        if (currentOpIsFinished() & (!activityHasBeenNotified)) {
            notifyActivityOpCompleted(currentOpResult);
        }
    }

    private void notifyActivityOpCompleted(OpResult result) {
        if (currentOpParams.forActivity != null) {
            currentOpParams.forActivity.onTaskCompleted(result);
            activityHasBeenNotified = true;
        }
    }

    private static LongOpsRunner ref;

    private LongOpsRunner() {
        this.execute();
    }

    public static synchronized LongOpsRunner getLongOpsRunner() {
        if (ref == null)
            ref = new LongOpsRunner();
        return ref;
    }

    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

}

I hope someone helps with making this work, as it would be very useful not only for me, but many other people out there. Thank you.

cdriver
  • 155
  • 1
  • 12
  • I can't understand what you're asking? Are you asking someone out here to create a test harness for your code and debug your code? Since you're saying 'there's no code that is currently using it'. Don't you want to test your own code yourself? – Alexander Kulyakhtin Apr 09 '12 at 11:56
  • I don't actually know how to test this. To test it, as far as i know, i should have 1,2 or 3 activities trying to do things with this singleton at very specific timings (to be able to test for data corruption or deadlocks) and i don't know how to time them or even how to consider every possibility. That's why i thought it would be nice if someone with adequate experience could see any problems without testing it. – cdriver Apr 09 '12 at 12:02

1 Answers1

0

Try Loaders. I switched from simple AsyncTasks to AsyncTaskLoaders and they solve lots of problems. If you implement a Loader as a standalone class, it would meet all of your requirements, especially when it comes to rotation which is the biggest issue with old AsyncTask.

Michał Klimczak
  • 12,674
  • 8
  • 66
  • 99
  • My application should be compatible with android 1.6, which has no loaders. There is a library somewhere (can't find it right now) that enables earlier versions of android to use features of later versions, but i hear it is full of bugs and problems. If that is not true, i could try using loaders. – cdriver Apr 09 '12 at 12:10
  • Btw, if there is no other answer better that this, i'll mark it as correct in a couple of days. – cdriver Apr 09 '12 at 12:12
  • It's called Android Compatibility Library (ACL). I use it now in my 2.1+ app, but I think it works back to 1.6. It's not full of bugs, it has several drawbacks. E.g. there is no Maps support for new features like Fragments and Loaders. So if you want to use it with maps, it's not that easy, but it's not impossible either - see my question: http://stackoverflow.com/questions/10037125/actionbarsherlock-maps-loaders-java-lang-noclassdeffounderror. It took me couple days, but I managed to get it all working together. – Michał Klimczak Apr 09 '12 at 12:21