Search code examples
javaandroidandroid-asynctaskcountdowntimerlooper

Running CountDownTimer inside AsyncTask throws java.lang.RuntimeException - Looper.prepare()


I have a .lrc file and I need to go over every line with a CountDownTimer. I have tried using AsyncTask to do so but I get the error:

Caused by: java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()

On line new CountDownTimer... I tried doing it with runnable but I still get the same error. My goal is to get it to go over every line in .lrc file which looks like this:

[00:04.15]Help me, it's like the walls are caving in
[00:10.46]Sometimes I feel like giving up
[00:13.63]But I just can't
...

I am not sure how efficient it is to do it the way I am trying to do. I am thinking of going through every line in the doInBackground(). If there is a better way to do it then let me know. But to begin with, why am I getting the EXCEPTION ?

Just to note.. I have simplified the code as much as I could so it would be easier to understand what I am trying to do.

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyView myView = new 
        myView.play();
    }
}

public class MyView{

    public void play() {
        new CustomAsync().execute();
    }
}

class CustomAsync extends AsyncTask<Lyric, Void, Void> {

    protected Void doInBackground(Lyric... param) {
        startLyricCountDownTimer(param);
        return null;
    }

    protected void onPostExecute(Void param) {
        //Print Toast or open dialog
    }

    private void startLyricCountDownTimer(Lyric lyric){

        new CountDownTimer(30000, 10) { //This is where it throws the error

            public void onTick(long millisUntilFinished) {
                //Do the thing
            }
            public void onFinish() {

            }
        }.start();
    }
}

EDIT Is it better to go with the AsyncTask and do like Son Truong suggested or to use the following code for each and every lrc line?

new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            new CountDownTimer(millisInFuture,countDownInterval) {
                @Override
                public void onTick(

Solution

  • CountDownTimer uses a Handler to post messages to a Message Queue of a Thread which has a Looper. onTick and onFinish will be called on which thread based on where you create CountDownTimer instance.

    In your case because you create CountDownTimer instance in doInBackground method of AsyncTask so these two methods will be call on AsyncTask thread.

    In constructor of CountDownTimer, it will create Handler instance as well. The Handler will check whether or not current thread has a Looper, if not it will throw a RuntimeException with message.

    Can't create handler inside thread that has not called Looper.prepare()

    Because AsyncTask uses a thread which has no Looper, that why your app crashes.

    My suggestion is in doInBackground method you open a connection to .lrc file and read each line, for each line read, use runOnUIThread to send the line to UI thread (then you can process the line read there by display a Toast on screen, etc).

    Update: I will demo how to read line by line from a file then display it on a text view each 3 seconds.

    First write a class which read from an inputstream line by line

    static class ReadLyricTask extends AsyncTask<InputStream, String, Void> {
    
        WeakReference<MainActivity> mMainActivity;
    
        ReadLyricTask(MainActivity activity) {
            mMainActivity = new WeakReference<>(activity);
        }
    
        @Override
        protected Void doInBackground(InputStream... inputStreams) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStreams[0]));
            String line;
            try {
                while ((line = reader.readLine()) != null) {
                    publishProgress(line);
                }
            } catch (IOException e) {
                // Do nothing.
            } finally {
                try {
                    inputStreams[0].close();
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    
        @Override
        protected void onProgressUpdate(String... values) {
            MainActivity activity = mMainActivity.get();
            if (activity != null) {
                activity.displayLyricLineOnTextView(values[0]);
            }
        }
    }
    

    Then just use it in MainActivity

    public class MainActivity extends AppCompatActivity {
        private static final int UPDATE_LYRIC_TEXT_INTERVAL = 3000; // Change lyric text each 3 seconds.
        private int mCurrentInterval = 0;
    
        private TextView mLyricTextView;
    
        private Handler mHandler = new Handler();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mLyricTextView = findViewById(R.id.lyricText);
    
            // I put a file named lyric.lrc in raw folder, for your case just open an input stream from a file.
            InputStream inputStream = getResources().openRawResource(R.raw.lyric);
            new ReadLyricTask(this).execute(inputStream);
        }
    
        private void displayLyricLineOnTextView(final String lyricLine) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mLyricTextView.setText(lyricLine);
                }
            }, mCurrentInterval);
    
            mCurrentInterval += UPDATE_LYRIC_TEXT_INTERVAL;
        }
    }