package de.nisble.droidsweeper.game.database;
import java.util.ArrayList;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import de.nisble.droidsweeper.config.Constants;
import de.nisble.droidsweeper.config.Level;
import de.nisble.droidsweeper.game.database.DSDBContract.GameTable;
import de.nisble.droidsweeper.game.replay.Replay;
import de.nisble.droidsweeper.utilities.LogDog;
import static de.nisble.droidsweeper.game.database.DSDBContract.*;
/** Adapter to ease the access to the database.
* @author Moritz Nisblé moritz.nisble@gmx.de */
public final class DSDBAdapter {
private static final String CLASSNAME = DSDBAdapter.class.getSimpleName();
public static final DSDBAdapter INSTANCE = new DSDBAdapter();
private DSDBHelper mHelper = null;
private DSDBAdapter() {
}
/** Initialization.
* @note Method isn't thread-safe.
* @param context Androids application context. */
public DSDBAdapter open(Context context) {
if (mHelper == null) {
mHelper = new DSDBHelper(context);
}
return this;
}
public void close() {
if (mHelper != null)
mHelper.close();
}
/** Get all stored games for a specific difficulty without the replay data.
* @param difficulty The difficulty.
* @return A with all entries for the given difficulty.
* @throws Exception On unknown difficulty. */
public ArrayList<DSDBGameEntry> getGames(Level level) throws Exception {
if (Level.CUSTOM == level) {
throw new IllegalArgumentException("No database entries for custom difficulty levels");
}
ArrayList<DSDBGameEntry> result = new ArrayList<DSDBGameEntry>();
SQLiteDatabase db = mHelper.getReadableDatabase();
Cursor c = db.rawQuery("SELECT * FROM " + GamesView.VIEW_NAME + " WHERE " + GamesView.CN_LEVEL + "=? "
+ " ORDER BY " + GamesView.CN_TIME + ", " + GamesView.CN_DATE,
new String[] { String.valueOf(level.ordinal()) });
try {
if (c.moveToFirst()) {
while (!c.isAfterLast()) {
DSDBGameEntry entry = new DSDBGameEntry(c.getLong(c.getColumnIndexOrThrow(GamesView._ID)), level,
c.getString(c.getColumnIndexOrThrow(GamesView.CN_NAME)), c.getLong(c
.getColumnIndexOrThrow(GamesView.CN_TIME)), c.getLong(c
.getColumnIndexOrThrow(GamesView.CN_DATE)), null);
result.add(entry);
c.moveToNext();
}
} else {
LogDog.d(CLASSNAME, "No entries available for level: " + level.toString());
}
} finally {
c.close();
}
return result;
}
public DSDBGameEntry getReplay(long gameID) throws Exception {
DSDBGameEntry entry = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
Cursor c = db.rawQuery("SELECT * FROM " + GamesView.VIEW_NAME + " WHERE " + GamesView._ID + "=? ",
new String[] { String.valueOf(gameID) });
try {
if (c.moveToFirst()) {
// Create an entry object with the information from GameView
entry = new DSDBGameEntry(c.getLong(c.getColumnIndexOrThrow(GamesView._ID)), Level.fromInt(c.getInt(c
.getColumnIndexOrThrow(GamesView.CN_LEVEL))), c.getString(c
.getColumnIndexOrThrow(GamesView.CN_NAME)), c.getLong(c
.getColumnIndexOrThrow(GamesView.CN_TIME)), c.getLong(c
.getColumnIndexOrThrow(GamesView.CN_DATE)), null);
} else {
throw new IllegalArgumentException("Unable to query replay for ID " + gameID);
}
} finally {
c.close();
}
// Use getReplay(DSDBGameEntry entry) to query the replay
return getReplay(entry);
}
/** Get the replay for a given DBEntry.
* @note The returned entry is not the same as the one passed in.
* Not thread-safe!
* @param entry The entry to query the replay for.
* @return A new entry including the replay data. */
public DSDBGameEntry getReplay(DSDBGameEntry entry) throws Exception {
if (Level.CUSTOM == entry.LEVEL) {
throw new IllegalArgumentException("No database entries for custom difficulty levels");
}
DSDBGameEntry result = null;
SQLiteDatabase db = mHelper.getReadableDatabase();
/* ID is sufficient for where clause. */
Cursor c = db.query(GameTable.TABLE_NAME, new String[] { GameTable._ID, GameTable.CN_REPLAY }, GameTable._ID
+ "=?", new String[] { String.valueOf(entry.GAMEID) }, null, null, null);
try {
if (c.moveToFirst()) {
/* Create a new entry from the given one and add the replay */
result = new DSDBGameEntry(entry.GAMEID, entry.LEVEL, entry.NAME, entry.PLAYTIME, entry.EPOCHTIME,
c.getBlob(c.getColumnIndex(GameTable.CN_REPLAY)));
} else {
throw new IllegalArgumentException("Unable to query replay for ID " + entry.GAMEID);
}
} finally {
c.close();
}
return result;
}
/** Check if a time would be inserted or not. The database isn't changed.
* @param difficulty The difficulty.
* @param milliseconds The time in ms.
* @return True on insert, else false. */
public boolean isHighScore(Level level, long milliseconds) throws Exception {
if (Level.CUSTOM == level) {
throw new IllegalArgumentException("No database entries for custom difficulty levels");
}
boolean result = false;
SQLiteDatabase db = mHelper.getReadableDatabase();
Cursor c = db.rawQuery("SELECT " + Level4GameView.CN_TIME + " FROM " + Level4GameView.VIEW_NAME + " WHERE "
+ Level4GameView.CN_LEVEL + "=? " + " ORDER BY " + Level4GameView.CN_TIME + ", "
+ Level4GameView.CN_DATE, new String[] { String.valueOf(level.ordinal()) });
try {
if (!c.moveToLast() || (c.getCount() < Constants.MAX_HIGHSCORES)
|| milliseconds < c.getLong(c.getColumnIndexOrThrow(Level4GameView.CN_TIME))) {
/* Highscore when: Less than 10 entries. Move to last is false
* (means there are no entries! If it is true we are now on the
* last entry.) The new time is faster (<) than the last
* one. (That means: No highscore on equal times. That should be
* the expected behavior.) */
result = true;
}
} finally {
c.close();
}
return result;
}
private void deleteExcessGames(Level level) {
SQLiteDatabase db = mHelper.getWritableDatabase();
/* Statement for getting all games for the given level.
* The ID is the one from the game table. */
String sqlGames = "SELECT " + Level4GameView._ID + ", " + Level4GameView.CN_GAMEPLAYER + " FROM "
+ Level4GameView.VIEW_NAME + " WHERE " + Level4GameView.CN_LEVEL + "="
+ String.valueOf(level.ordinal()) + " ORDER BY " + Level4GameView.CN_TIME + ", "
+ Level4GameView.CN_DATE;
Cursor c = null;
try {
/* Query in loop while the cursor isn't empty and the count of games
* is bigger than the configured maximum */
while ((c = db.rawQuery(sqlGames, null)).moveToLast() && c.getCount() > Constants.MAX_HIGHSCORES) {
int result = 0;
long gameID = c.getLong(c.getColumnIndexOrThrow(Level4GameView._ID));
/* Save the playerID foreign key */
long playerID = c.getLong(c.getColumnIndexOrThrow(Level4GameView.CN_GAMEPLAYER));
/* Delete the game */
result = db.delete(GameTable.TABLE_NAME, GameTable._ID + "=" + gameID, null);
if (result > 0)
LogDog.i(CLASSNAME, "Game " + gameID + " dropped.");
/* Try to delete the player. DBMS restricts this when there are
* other games referencing this player! */
try {
result = db.delete(PlayerTable.TABLE_NAME, PlayerTable._ID + "=" + playerID, null);
if (result > 0)
LogDog.i(CLASSNAME, "Player " + playerID + " dropped.");
} catch (SQLiteConstraintException e) {
LogDog.i(CLASSNAME, "Unable to drop player " + playerID + ". Player is still referenced by other games.");
}
}
} finally {
if (c != null)
c.close();
}
}
/** Insert a new entry.
* @note Database removes the slowest time by trigger when more than 10
* entries are made. Use isHighscore() to check if the time would be
* inserted before a call to this function.
* @param entry An entry.
* @throws Exception On unknown difficulty.
* @return The ID of the inserted game. */
public long insertTime(Replay replay) throws Exception {
if (Level.CUSTOM == replay.getGameConfig().LEVEL) {
throw new IllegalArgumentException("No database entries for custom difficulty levels");
} else if (0 == replay.getName().length()) {
throw new IllegalArgumentException("No player name set!");
}
ContentValues values = new ContentValues();
long playerID = -1;
long gameID = -1;
SQLiteDatabase db = mHelper.getWritableDatabase();
// Query player name
Cursor c = db.query(PlayerTable.TABLE_NAME, new String[] { PlayerTable._ID, PlayerTable.CN_NAME },
PlayerTable.CN_NAME + "=?", new String[] { replay.getName() }, null, null, null);
/* Use a transaction to not pollute the player table with names
* that are not linked with a game, if something goes wrong and the
* game insert fails. */
db.beginTransaction();
try {
/* Check whether the cursor is empty */
if (!c.moveToFirst()) {
/* Empty! Player name not found. Insert new one. */
values.put(PlayerTable.CN_NAME, replay.getName());
playerID = db.insertOrThrow(PlayerTable.TABLE_NAME, null, values);
values.clear();
} else if (c.getCount() > 1) {
throw new SQLException("Player table inconsistant. Broken or manipulated database.");
} else {
/* Player name found. Store row id. */
playerID = c.getLong(c.getColumnIndexOrThrow(PlayerTable._ID));
}
/* A query for the level is redundant. The level number is the
* foreign key! */
values.put(GameTable.CN_GAMELEVEL, replay.getGameConfig().LEVEL.ordinal());
values.put(GameTable.CN_GAMEPLAYER, playerID);
values.put(GameTable.CN_TIME, replay.getPlayTime());
values.put(GameTable.CN_DATE, replay.getEpochTime());
values.put(GameTable.CN_REPLAY, replay.serializeTimeSteps());
gameID = db.insertOrThrow(GameTable.TABLE_NAME, null, values);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
c.close();
}
deleteExcessGames(replay.getGameConfig().LEVEL);
return gameID;
}
}