Search code examples
androidandroid-asynctaskcancellation

Android/AsyncTask: Do you still have to check "isCancelled" (API 24)?


My app uses an AsyncTask to download files while displaying a ProgressDialog (I'm aware that it's deprecated) with a "Cancel" button.

According to this you should check isCancelled() in doInBackground periodically because mytask.cancel(true) won't interrupt doInBackground on its own.

I simply cancelled the task without checking at first and noticed that it still stops doInBackground: Depending on how long I let it download before pressing the "Cancel" button, I've seen different sizes in the resulting file - from just a few kb to a couple of mb - the final size would have been around 9mb.

How is this possible? Do you actually not have to call isCancelled() anymore?

My AsyncTask:

private class DownloadTask extends AsyncTask<String, String, String> {
    protected void onPreExecute() {
        progressdialog.setMessage("Preparing Download...");
        progressdialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressdialog.setProgressNumberFormat(null);
        progressdialog.setProgressPercentFormat(null);
        progressdialog.setIndeterminate(true);
        progressdialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                progressdialog.dismiss();
                mytask.cancel(true);
            }
        });
        progressdialog.show();
    }

    protected String doInBackground(String... bla) {
        String error = download();
        return error;
    }

    protected void onProgressUpdate(String... s) {
        //....
    }

    protected void onPostExecute(String s) {
        progressdialog.dismiss();
        //....
    }

Solution

  • According to this you should check isCancelled() in doInBackground periodically because mytask.cancel(true) won't interrupt doInBackground on its own.

    Actually it is not true.

    According to documentation:

    After invoking this method, you should check the value returned by isCancelled() periodically from doInBackground(Object[]) to finish the task as early as possible.

    It means you can additionally check for isCancelled() to stop AsyncTask earlier if it is started.

    mytask.cancel(true) will stop execution anyway.

    Let`s see under the hood what is going on

    When you call mytask.cancel(true):

    public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        return mFuture.cancel(mayInterruptIfRunning);
    }
    

    Where mFuture is FutureTask that holds runnable inside

    Then mFuture.cancel is called:

    public boolean cancel(boolean mayInterruptIfRunning) {
        if (state != NEW)
            return false;
        if (mayInterruptIfRunning) {
            if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, INTERRUPTING))
                return false;
            Thread t = runner;
            if (t != null)
                t.interrupt();
            UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); // final state
        }
        else if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED))
            return false;
        finishCompletion();
        return true;
    }
    

    Where runner is just

    private volatile Thread runner;
    

    Since its just thread, lets see what interrupt does in your case:

    If this thread is blocked in an I/O operation upon an interruptible channel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a ClosedByInterruptException.

    So if your download() method uses InterruptibleChannel interrupt will work.

    In other words looks like you have never had to call isCancelled() to interrupt AsyncTask =) since Thread.interrupt can stop io blocking operation in your case.