/*
Copyright (C) 2013 Haowen Ning
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.liberty.android.fantastischmemo.service.cardplayer;
import android.util.Log;
import com.google.common.base.Objects;
import org.liberty.android.fantastischmemo.entity.Card;
import org.liberty.android.fantastischmemo.tts.AnyMemoTTS;
/*
* State object representing the state machine of card player
* The normal flow is STOPPED -> PLAYING_QUESTION -> PLAYING_ANSWER -> PLAYING_QUESTION ...
* If START_PLAYING is received, the context will transit to PLAYING_QUESTION
* If STOP_PLAYING is received, any state of context will transit to STOPPED
*/
public enum CardPlayerState implements CardPlayerStateTransition {
STOPPED {
public void transition(CardPlayerContext context, CardPlayerMessage message) {
switch(message) {
case START_PLAYING:
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case GO_TO_NEXT:
// In STOPPED state, GO_TO_NEXT / GO_TO_PREV message is still handled
// because it need to change the current card in the context and callback
// the handler so the UI will change card without actually speaking.
Card nextCard = findNextCard(context);
if (nextCard == null) {
break;
}
context.setCurrentCard(nextCard);
context.getEventHandler().onPlayCard(context.getCurrentCard());
break;
case GO_TO_PREV:
Card prevCard = findPrevCard(context);
if (prevCard == null) {
break;
}
context.setCurrentCard(prevCard);
context.getEventHandler().onPlayCard(context.getCurrentCard());
break;
default:
// Once it is in STOPPED state, no call other than START_PLAYING can go through.
break;
}
}
},
PLAYING_QUESTION {
public void transition(CardPlayerContext context, CardPlayerMessage message) {
switch(message) {
case GO_TO_NEXT:
context.getCardTTSUtil().stopSpeak();
Card nextCard = findNextCard(context);
// Always check null card, if it happens it means no card can be played so
// it will send STOP_PLAYING message.
if (nextCard == null) {
context.getState().transition(context, CardPlayerMessage.STOP_PLAYING);
break;
}
context.setCurrentCard(nextCard);
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case GO_TO_PREV:
context.getCardTTSUtil().stopSpeak();
Card prevCard = findPrevCard(context);
if (prevCard == null) {
context.getState().transition(context, CardPlayerMessage.STOP_PLAYING);
break;
}
context.setCurrentCard(prevCard);
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case PLAYING_ANSWER_COMPLETED:
Log.w("CardPlayerState", "Wrong state, the question is playing but receive message that answer completed!");
assert false : "Wrong state";
break;
case PLAYING_QUESTION_COMPLETED:
context.setState(PLAYING_ANSWER);
playAnswer(context);
break;
case STOP_PLAYING:
stopPlaying(context);
break;
default:
break;
}
}
},
PLAYING_ANSWER {
public void transition(CardPlayerContext context, CardPlayerMessage message) {
switch(message) {
case GO_TO_NEXT:
context.getCardTTSUtil().stopSpeak();
Card nextCard = findNextCard(context);
if (nextCard == null) {
context.getState().transition(context, CardPlayerMessage.STOP_PLAYING);
break;
}
context.setCurrentCard(nextCard);
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case GO_TO_PREV:
context.getCardTTSUtil().stopSpeak();
Card prevCard = findPrevCard(context);
if (prevCard == null) {
context.getState().transition(context, CardPlayerMessage.STOP_PLAYING);
break;
}
context.setCurrentCard(prevCard);
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case PLAYING_ANSWER_COMPLETED:
nextCard = findNextCard(context);
if (nextCard == null) {
stopPlaying(context);
break;
}
context.setCurrentCard(nextCard);
context.setState(PLAYING_QUESTION);
playQuestion(context);
break;
case PLAYING_QUESTION_COMPLETED:
Log.w("CardPlayerState", "Wrong state, the answer is playing but receive message that question completed!");
assert false : "Wrong state";
break;
case STOP_PLAYING:
stopPlaying(context);
break;
default:
break;
}
}
};
private static void playQuestion(final CardPlayerContext context) {
Log.v("CardPlayerState", "Playing Question: " + context.getCurrentCard().getId());
// Callback to the handler first since the actual TTS call would take some time.
// We usually need UI to update first.
context.getEventHandler().onPlayCard(context.getCurrentCard());
context.getCardTTSUtil().speakCardQuestion(context.getCurrentCard(),
new AnyMemoTTS.OnTextToSpeechCompletedListener() {
public void onTextToSpeechCompleted(final String text) {
// Use UI thread's handler to post call instead of sleeping.
// Note the TTS is running on its own thread.
context.getAmTTSServiceHandler().postDelayed(new Runnable() {
public void run() {
// Make sure the card is still current.
// If the card changed, it is most likely the fast foward / backward
// function is needed.
if (Objects.equal(context.getCurrentCard().getQuestion(), text)) {
Log.v("CardPlayerState", "Playing question completed for id " + context.getCurrentCard().getId());
context.getState().transition(context, CardPlayerMessage.PLAYING_QUESTION_COMPLETED);
}
}
}, context.getDelayBeteenQAInSec() * 1000);
}
});
}
private static void playAnswer(final CardPlayerContext context) {
Log.v("CardPlayerState", "Playing Answer: " + context.getCurrentCard().getId());
context.getEventHandler().onPlayCard(context.getCurrentCard());
context.getCardTTSUtil().speakCardAnswer(context.getCurrentCard(),
new AnyMemoTTS.OnTextToSpeechCompletedListener() {
public void onTextToSpeechCompleted(final String text) {
context.getAmTTSServiceHandler().postDelayed(new Runnable() {
public void run() {
if (Objects.equal(context.getCurrentCard().getAnswer(), text)) {
Log.v("CardPlayerState", "Playing answer completed for id " + context.getCurrentCard().getId());
context.getState().transition(context, CardPlayerMessage.PLAYING_ANSWER_COMPLETED);
}
}
}, context.getDelayBeteenCardsInSec() * 1000);
}
});
}
/* This method will return null if there is no card to play */
private static Card findNextCard(final CardPlayerContext context) {
Card card;
if (context.getShuffle()) {
card = context.getDbOpenHelper().getCardDao().getRandomCards(null, 1).get(0);
} else {
// Get the next ordinal card
card = context.getDbOpenHelper().getCardDao().queryNextCard(context.getCurrentCard());
// Do not repeat from the beginning if the repeat is set
if (!context.getRepeat() && card.getOrdinal() <= context.getCurrentCard().getOrdinal()) {
return null;
}
}
assert card != null : "Next card should not be null";
context.getDbOpenHelper().getLearningDataDao().refresh(card.getLearningData());
context.getDbOpenHelper().getCategoryDao().refresh(card.getCategory());
return card;
}
/* This method will return null if there is no card to play */
private static Card findPrevCard(final CardPlayerContext context) {
Card card = null;
if (context.getShuffle()) {
card = context.getDbOpenHelper().getCardDao().getRandomCards(null, 1).get(0);
} else {
// Get the next ordinal card
card = context.getDbOpenHelper().getCardDao().queryPrevCard(context.getCurrentCard());
// Do not repeat from the beginning if the repeat is set
if (!context.getRepeat() && card.getOrdinal() >= context.getCurrentCard().getOrdinal()) {
return null;
}
}
assert card != null : "Prev card should not be null";
context.getDbOpenHelper().getLearningDataDao().refresh(card.getLearningData());
context.getDbOpenHelper().getCategoryDao().refresh(card.getCategory());
return card;
}
private static void stopPlaying(final CardPlayerContext context) {
context.setState(STOPPED);
context.getEventHandler().onStopPlaying();
}
}