Friday, June 10, 2011

Android - Timer issues

I'm in the process of setting up an "autoplay" function for my quiz app. The idea is that, when an answer is displayed, it should wait a certain amount of time, then advance automatically.

This is kind of the inverse of what it's doing right now - it's displaying the question until a countdown time expires, then showing the correct answer and displaying the question.

So, what I really want to do is reduce or eliminate the timer in the question mode, and add the timer in the answer mode.

Eliminating the timer in question mode has shown to be somewhat problematic. The problem is that the list view is not getting loaded properly unless the timer takes say two seconds or more. This is strange to say the least, because the list view is getting instantiated beforehand. For that matter, at one point there was no timer - the question would just sit there. I doubt that even if I did things *really fast* that I would get a crash.

Maybe I should some how test the ListView for its state to make sure it's ok before calling the timer. Like, get the number of children, or something. It was actually crashing on the last line of this sequence:

int childCount = listView.getChildCount();
int lastViewPos = childCount - 1;


View correctRow = null;

int i = findCorrectViewAnswerPosition(listView, childCount);

if (mAppState.chosenAnswer.equalsIgnoreCase(TIMEOUT_ANSWER)) {
correctRow = listView.getChildAt(i);
} else {
correctRow = listView.getChildAt(lastViewPos - i);
}

correctRow.setBackgroundResource(R.drawable.hilight_gradient_correct);

Since it's a timeout, and since the null is on correct row, and it's going through the timeout answer, it must be failing to find any children. That seems to be what it is.

Let's test this. This code will definitely crash:

/* attach the adapter to the ListView */
setListAdapter(adapter);

/* set ListView's choice mode to single */
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);

startCountdownTimer();

if (mAppState.autoPlay){
this.handleTimeOut();
}

Yup. It crashed and the child count was zero.

Now, let's try this. In the timer, every time it counts down, check the child for zero. If it's zero, and in autoplay, don't do anything; otherwise, call the handle timeout.

if (mAppState.autoPlay){
if (getListView().getChildCount() <= 0){
// no op
} else {
onFinish();
}
}

Let's try it.

Ah, that's better. It launched a *bunch* of audios before finally crashing.

Now, the next question is, how can I just make it wait until the end of one question before launching the next?

Right now, the code is like this:

// automatically advance on autoplay
if (mAppState.autoPlay) {
mPlaySoundUtil.playSound(mQuestion);
this.advanceQuestion();
} else {
mPlaySoundUtil.stopPlay();
mPlaySoundUtil.playSound(mQuestion);
}

What would be really nice is if I could have the media notify me somehow when it's done playing. Let's check it.

Here's how we're playing sound files:

private boolean playSoundFile(File file) {

Log.d(TAG, "playSoundFile start");

mp = new MediaPlayer();
try {
mp.setDataSource(file.getAbsolutePath());
mp.prepare();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mp.start();

Log.d(TAG, "playSoundFile end");
return true;
}

Let's look up MediaPlayer.

Well, lokee here.

Interface MediaPlayer.OnCompletionListener Interface definition for a callback to be invoked when playback of a media source has completed.

We can do this:

import android.media.MediaPlayer.OnCompletionListener;

public class QuestionActivity extends ListActivity implements OnCompletionListener {

Then, we can do this:

@Override
public void onCompletion(MediaPlayer mp) {
// TODO Auto-generated method stub
this.advanceQuestion();
}

Now, we just have to set it on the media player:

Then, working backwards from the method that plays the sound:

/**
* @param file
*/
private boolean playSoundFile(File file, OnCompletionListener cl ) {

Log.d(TAG, "playSoundFile start");

mp = new MediaPlayer();
try {
mp.setDataSource(file.getAbsolutePath());
mp.prepare();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
mp.setOnCompletionListener(cl);

mp.start();
Log.d(TAG, "playSoundFile end");
return true;
}


To the method that calls the player:

// package visibility, played by Question Activity

boolean playSound(Question question, OnCompletionListener cl) {

Log.d(TAG, "playSound");

String myDir = null;

if (question.level == 4){
myDir = "aud4";
} else if (question.level == 5){
myDir = "aud5";
}

String soundFileName = StartActivity.ROOT_PATH + "/" + myDir + "/"
+ question.level + "_" + question.number + ".mp3";

File soundFile = new File(soundFileName);

if (soundFile.exists()) {
playSoundFile(soundFile, cl);
} else {
Log.d(TAG, "file notfound: " + soundFile.getAbsolutePath() + ", " + question.level + " "
+ question.hiragana + " " + question.english);
}

Log.d(TAG, "playSound end");

return true;

}


So, the only thing to clean up now is the fact that we are calling advance to next question automatically in both cases; we only want to do that in the case of the autoplay:

@Override
public void onCompletion(MediaPlayer mp) {

if (mAppState.autoPlay) {
this.advanceQuestion();
}
}


Let's give it a try!

Hmmm...still kind of messed up. It seems like it's still launching a bunch of audios fast, but similar ones. Right; because the callback doesn't actually make it wait. But since the question hasn't advanced, it's calling the same one, or something.

Well, I could set an infinite loop in a timer that waits for the callback, by checking a flag set by the call back. Max out at say 10 seconds, in the case the audio file isn't found, which there are still a few of.

This QuestionActivity class is really getting unwieldy. I need to pull out the various methods into separate groupings of classes for ease, organization, and comprehensibility.

What I'll do, is I'll extracte the timer class into a separate class from QuestionActivity. Then model the new timer class on that.

Hmmm. The thing is, it operates on some of the internals of Question Activity - like the progressBar,

I'll deal with that later.

Hmmm...somehow it's looping on the same playSound - like an echo. The play sound is being called in a loop?

Let's pull out the call to finish on the childCount > 0;

Wow. That is very close to the behavior I want. It highlights the correct answer; it's leaving the display up for a bit - of the correct word; and it's auto advancing I just need to kill the 10 second delay. But, what is causing the advance?

It's almost certainly this:

@Override
public void onCompletion(MediaPlayer mp) {

if (mAppState.autoPlay) {
this.advanceQuestion();
}
}


Which is perfect. Somehow, calling onFinish within the timer is fouling things up. all I really need to do, is change the countdown timer to a respectable interval, just faster than the plodding 10 seconds in quiz mode. Hmm..I wish I could just test for list activity children.

Well, let's give it, say, 3 seconds. Ok, that works. Actually, I now have it down to 1 second.

final int totalMsecs; // 10 seconds
int callInterval;

if (this.mAppState.autoPlay) {
totalMsecs = 1 * 1000; // 1 second
callInterval = 100;
} else {

totalMsecs = 10 * 1000; // 10 seconds
callInterval = 100;

}

So, it's one second plus the time it takes the media to play. That's pretty satisfactory. I can get it down to even more. But - there is some monkeying around I'll have to do to cover the case where the file's not found. But, for now, it's looking pretty good :)

No comments:

Post a Comment