/*
Copyright (C) 2012 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.queue;
import android.content.Context;
import android.util.Log;
import org.liberty.android.fantastischmemo.common.AnyMemoDBOpenHelper;
import org.liberty.android.fantastischmemo.common.AnyMemoDBOpenHelperManager;
import org.liberty.android.fantastischmemo.dao.CardDao;
import org.liberty.android.fantastischmemo.dao.LearningDataDao;
import org.liberty.android.fantastischmemo.entity.Card;
import org.liberty.android.fantastischmemo.entity.Category;
import org.liberty.android.fantastischmemo.entity.ReviewOrdering;
import org.liberty.android.fantastischmemo.scheduler.Scheduler;
import org.liberty.android.fantastischmemo.utils.AnyMemoExecutor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
public class LearnQueueManager implements QueueManager {
private static final String TAG = LearnQueueManager.class.getSimpleName();
/*
* The scheduler to determine whether a card should reimain
* in the learn queue
*/
private Scheduler scheduler;
private Category filterCategory;
private List<Card> learnQueue;
private List<Card> newCache;
private List<Card> reviewCache;
private BlockingQueue<Card> dirtyCache;
private int learnQueueSize;
private int cacheSize;
private boolean shuffle;
private ReviewOrdering reviewOrdering;
private Context context;
private String dbPath;
private LearnQueueManager(Builder builder) {
this.filterCategory = builder.filterCategory;
this.learnQueueSize = builder.learnQueueSize;
this.cacheSize = builder.cacheSize;
this.shuffle = builder.shuffle;
this.scheduler = builder.scheduler;
this.context = builder.context;
this.dbPath = builder.dbPath;
this.reviewOrdering = builder.reviewOrdering;
learnQueue = Collections.synchronizedList(new LinkedList<Card>());
newCache = Collections.synchronizedList(new LinkedList<Card>());
reviewCache = Collections.synchronizedList(new LinkedList<Card>());
// Make sure the dirtyCache is thread safe because multiple threads will access
// the set
dirtyCache = new LinkedBlockingQueue<Card>();
}
@Override
public synchronized Card dequeue() {
shuffle();
if (!learnQueue.isEmpty()) {
Card c = learnQueue.get(0);
Log.d(TAG, "Dequeue card: " + c.getId());
return c;
} else {
return null;
}
}
@Override
public synchronized Card dequeuePosition(int cardId) {
position(cardId);
if (!learnQueue.isEmpty()) {
Card c = learnQueue.get(0);
Log.d(TAG, "Dequeue card: " + c.getId());
return c;
} else {
return null;
}
}
@Override
public synchronized void remove(Card card) {
learnQueue.remove(card);
reviewCache.remove(card);
newCache.remove(card);
}
@Override
public synchronized void release() {
// Make sure the cache is flushed.
flushDirtyCache();
AnyMemoExecutor.waitAllTasks();
}
private synchronized void refill() {
final AnyMemoDBOpenHelper dbOpenHelper = AnyMemoDBOpenHelperManager
.getHelper(context, dbPath);
final CardDao cardDao = dbOpenHelper.getCardDao();
dumpLearnQueue();
List<Card> exclusionList = new ArrayList<Card>(learnQueue.size()
+ dirtyCache.size());
exclusionList.addAll(learnQueue);
exclusionList.addAll(dirtyCache);
try {
if (newCache.size() == 0) {
List<Card> cs = cardDao.getNewCards(filterCategory,
exclusionList, cacheSize);
if (cs.size() > 0) {
newCache.addAll(cs);
}
}
if (reviewCache.size() == 0) {
List<Card> cs = cardDao.getCardsForReview(filterCategory,
exclusionList, cacheSize, reviewOrdering);
if (cs.size() > 0) {
reviewCache.addAll(cs);
}
}
while (learnQueue.size() < learnQueueSize && !reviewCache.isEmpty()) {
learnQueue.add(reviewCache.get(0));
reviewCache.remove(0);
}
while (learnQueue.size() < learnQueueSize && !newCache.isEmpty()) {
learnQueue.add(newCache.get(0));
newCache.remove(0);
}
} finally {
AnyMemoDBOpenHelperManager.releaseHelper(dbOpenHelper);
}
flushDirtyCache();
dumpLearnQueue();
}
private synchronized void shuffle() {
// Shuffle all the caches
if (shuffle) {
Collections.shuffle(newCache);
Collections.shuffle(reviewCache);
Collections.shuffle(learnQueue);
}
}
@Override
public synchronized void update(Card card) {
// Make sure to remove the stale cache first.
// Set.add(Object) will not overwrite object.
remove(card);
if (!scheduler.isCardLearned(card.getLearningData())) {
// Add to the back of the queue
learnQueue.add(card);
}
try {
dirtyCache.put(card);
} catch (InterruptedException e) {
Log.e(TAG, "Updating card is interrupted", e);
assert false : "The update should not be interrupted";
}
refill();
}
private synchronized void position(int cardId) {
Iterator<Card> learnIterator= learnQueue.iterator();
Iterator<Card> reviewCacheIterator = reviewCache.iterator();
Iterator<Card> newCacheIterator = newCache.iterator();
int learnQueueRotateDistance = 0;
while (learnIterator.hasNext()) {
Card c =learnIterator.next();
if (c.getId() == cardId) {
int index = learnQueue.indexOf(c);
learnQueueRotateDistance = -index;
Log.i(TAG, "Rotate index: " + index);
}
}
Collections.rotate(learnQueue, learnQueueRotateDistance);
while (reviewCacheIterator.hasNext()) {
Card c =reviewCacheIterator.next();
if (c.getId() == cardId) {
reviewCacheIterator.remove();
}
}
while (newCacheIterator.hasNext()) {
Card c = newCacheIterator.next();
if (c.getId() == cardId) {
newCacheIterator.remove();
}
}
final AnyMemoDBOpenHelper dbOpenHelper = AnyMemoDBOpenHelperManager.getHelper(context, dbPath);
final CardDao cardDao = dbOpenHelper.getCardDao();
final LearningDataDao learningDataDao = dbOpenHelper.getLearningDataDao();
try {
Card headCard = null;
headCard = cardDao.queryForId(cardId);
learningDataDao.refresh(headCard.getLearningData());
learnQueue.add(0, headCard);
} finally {
AnyMemoDBOpenHelperManager.releaseHelper(dbOpenHelper);
}
}
private synchronized void flushDirtyCache() {
AnyMemoExecutor.submit(new Runnable() {
public void run() {
// Update the queue
final AnyMemoDBOpenHelper dbOpenHelper = AnyMemoDBOpenHelperManager.getHelper(context, dbPath);
final CardDao cardDao = dbOpenHelper.getCardDao();
final LearningDataDao learningDataDao = dbOpenHelper.getLearningDataDao();
try {
learningDataDao.callBatchTasks (
new Callable<Void>() {
public Void call() throws Exception {
Log.i(TAG, "Flushing dirty cache. # of cards to flush: " + dirtyCache.size());
while (!dirtyCache.isEmpty()) {
Card card = dirtyCache.take();
Log.i(TAG, "Flushing card id: " + card.getId() + " with learning data: " + card.getLearningData());
if (learningDataDao.update(card.getLearningData()) == 0) {
Log.w(TAG, "LearningDataDao update failed for : " + card.getLearningData());
throw new RuntimeException("LearningDataDao update failed! LearningData to update: " + card.getLearningData() + " current value: " + learningDataDao.queryForId(card.getLearningData().getId()));
}
if (cardDao.update(card) == 0) {
Log.w(TAG, "CardDao update failed for : " + card.getLearningData());
throw new RuntimeException("CardDao update failed. Card to update: " + card);
}
}
Log.i(TAG, "Flushing dirty cache done.");
return null;
}
});
} finally {
AnyMemoDBOpenHelperManager.releaseHelper(dbOpenHelper);
}
}
});
}
private void dumpLearnQueue() {
StringBuilder sb = new StringBuilder();
sb.append("LearnQueue : card ids[");
for (Card c : learnQueue) {
sb.append("" + c.getId() + ", ");
}
sb.append("]\n");
// sb.append("LearnQueue: card learning data[");
// for (Card c : learnQueue) {
// sb.append("" + c.getLearningData() + "\n ");
// }
// sb.append("]\n");
sb.append("Dirty cache: card ids[");
for (Card c : dirtyCache) {
sb.append("" + c.getId() + ", ");
}
sb.append("]\n");
// sb.append("Dirty cache: card learning data[");
// for (Card c : dirtyCache) {
// sb.append("" + c.getLearningData() + "\n ");
// }
// sb.append("]\n");
Log.v(TAG, sb.toString());
}
public static class Builder {
private Scheduler scheduler;
private Category filterCategory;
private int learnQueueSize = 10;
private int cacheSize = 50;
private boolean shuffle = false;
private String dbPath;
private Context context;
private ReviewOrdering reviewOrdering;
public Builder(Context context, String dbPath) {
this.dbPath = dbPath;
this.context = context;
}
public Builder setScheduler(Scheduler scheduler) {
this.scheduler = scheduler;
return this;
}
public Builder setFilterCategory(Category filterCategory) {
this.filterCategory = filterCategory;
return this;
}
public Builder setLearnQueueSize(int learnQueueSize) {
this.learnQueueSize = learnQueueSize;
return this;
}
public Builder setCacheSize(int cacheSize) {
this.cacheSize = cacheSize;
return this;
}
public Builder setShuffle(boolean shuffle) {
this.shuffle = shuffle;
return this;
}
public Builder setReviewOrdering(ReviewOrdering reviewOrdering) {
this.reviewOrdering = reviewOrdering;
return this;
}
public QueueManager build() {
LearnQueueManager qm = new LearnQueueManager(this);
qm.refill();
return qm;
}
}
}