package de.blau.android.services.util;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import org.acra.ACRA;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDiskIOException;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import android.os.Build;
import android.util.Log;
import de.blau.android.R;
import de.blau.android.services.exceptions.EmptyCacheException;
import de.blau.android.util.Snack;
import de.blau.android.views.util.MapViewConstants;
/**
* The OpenStreetMapTileProviderDataBase contains a table with info for the available renderers and one for
* the available tiles in the file system cache.<br/>
* This class was taken from OpenStreetMapViewer (original package org.andnav.osm) in 2010
* by Marcus Wolschon to be integrated into the de.blau.androin
* OSMEditor.
* @author Nicolas Gramlich
* @author Marcus Wolschon <Marcus@Wolschon.biz
*/
public class MapTileProviderDataBase implements MapViewConstants {
private static final String DEBUG_TAG = MapTileProviderDataBase.class.getSimpleName();
private static final String DATABASE_NAME = "osmaptilefscache_db";
private static final int DATABASE_VERSION = 8;
private static final String T_FSCACHE = "tiles";
private static final String T_FSCACHE_RENDERER_ID = "rendererID";
private static final String T_FSCACHE_ZOOM_LEVEL = "zoom_level";
private static final String T_FSCACHE_TILE_X = "tile_column";
private static final String T_FSCACHE_TILE_Y = "tile_row";
// private static final String T_FSCACHE_LINK = "link"; // TODO store link (multiple use for similar tiles)
private static final String T_FSCACHE_TIMESTAMP = "timestamp";
private static final String T_FSCACHE_USAGECOUNT = "countused";
private static final String T_FSCACHE_FILESIZE = "filesize";
private static final String T_FSCACHE_DATA = "tile_data";
private static final String T_RENDERER = "t_renderer";
private static final String T_RENDERER_ID = "id";
private static final String T_RENDERER_NAME = "name";
private static final String T_RENDERER_BASE_URL = "base_url";
private static final String T_RENDERER_ZOOM_MIN = "zoom_min";
private static final String T_RENDERER_ZOOM_MAX = "zoom_max";
private static final String T_RENDERER_TILE_SIZE_LOG = "tile_size_log";
private static final String T_FSCACHE_CREATE_COMMAND = "CREATE TABLE IF NOT EXISTS " + T_FSCACHE
+ " ("
+ T_FSCACHE_RENDERER_ID + " VARCHAR(255) NOT NULL,"
+ T_FSCACHE_ZOOM_LEVEL + " INTEGER NOT NULL,"
+ T_FSCACHE_TILE_X + " INTEGER NOT NULL,"
+ T_FSCACHE_TILE_Y + " INTEGER NOT NULL,"
+ T_FSCACHE_TIMESTAMP + " INTEGER NOT NULL,"
+ T_FSCACHE_USAGECOUNT + " INTEGER NOT NULL DEFAULT 1,"
+ T_FSCACHE_FILESIZE + " INTEGER NOT NULL,"
+ T_FSCACHE_DATA + " BLOB,"
+ " PRIMARY KEY(" + T_FSCACHE_RENDERER_ID + ","
+ T_FSCACHE_ZOOM_LEVEL + ","
+ T_FSCACHE_TILE_X + ","
+ T_FSCACHE_TILE_Y + ")"
+ ");";
private static final String T_RENDERER_CREATE_COMMAND = "CREATE TABLE IF NOT EXISTS " + T_RENDERER
+ " ("
+ T_RENDERER_ID + " VARCHAR(255) PRIMARY KEY,"
+ T_RENDERER_NAME + " VARCHAR(255),"
+ T_RENDERER_BASE_URL + " VARCHAR(255),"
+ T_RENDERER_ZOOM_MIN + " INTEGER NOT NULL,"
+ T_RENDERER_ZOOM_MAX + " INTEGER NOT NULL,"
+ T_RENDERER_TILE_SIZE_LOG + " INTEGER NOT NULL"
+ ");";
private static final String SQL_ARG = "=?";
private static final String AND = " AND ";
private static final String T_FSCACHE_WHERE = T_FSCACHE_RENDERER_ID + SQL_ARG + AND
+ T_FSCACHE_ZOOM_LEVEL + SQL_ARG + AND
+ T_FSCACHE_TILE_X + SQL_ARG + AND
+ T_FSCACHE_TILE_Y + SQL_ARG;
private static final String T_FSCACHE_WHERE_INVALID = T_FSCACHE_RENDERER_ID + SQL_ARG + AND
+ T_FSCACHE_ZOOM_LEVEL + SQL_ARG + AND
+ T_FSCACHE_TILE_X + SQL_ARG + AND
+ T_FSCACHE_TILE_Y + SQL_ARG + AND
+ T_FSCACHE_FILESIZE + "=0";
private static final String T_FSCACHE_WHERE_NOT_INVALID = T_FSCACHE_RENDERER_ID + SQL_ARG + AND
+ T_FSCACHE_ZOOM_LEVEL + SQL_ARG + AND
+ T_FSCACHE_TILE_X + SQL_ARG + AND
+ T_FSCACHE_TILE_Y + SQL_ARG + AND
+ T_FSCACHE_FILESIZE + ">0";
private static final String T_FSCACHE_SELECT_LEAST_USED = "SELECT " + T_FSCACHE_RENDERER_ID + "," + T_FSCACHE_ZOOM_LEVEL + "," + T_FSCACHE_TILE_X + "," + T_FSCACHE_TILE_Y + "," + T_FSCACHE_FILESIZE + " FROM " + T_FSCACHE + " WHERE " + T_FSCACHE_USAGECOUNT + " = (SELECT MIN(" + T_FSCACHE_USAGECOUNT + ") FROM " + T_FSCACHE + ")";
private static final String T_FSCACHE_SELECT_OLDEST = "SELECT " + T_FSCACHE_RENDERER_ID + "," + T_FSCACHE_ZOOM_LEVEL + "," + T_FSCACHE_TILE_X + "," + T_FSCACHE_TILE_Y + "," + T_FSCACHE_FILESIZE + " FROM " + T_FSCACHE + " WHERE " + T_FSCACHE_FILESIZE + " > 0 ORDER BY " + T_FSCACHE_TIMESTAMP + " ASC";
private static final String T_FSCACHE_INCREMENT_USE = "UPDATE " + T_FSCACHE +" SET " + T_FSCACHE_USAGECOUNT + "=" + T_FSCACHE_USAGECOUNT + "+1, "
+ T_FSCACHE_TIMESTAMP + "=" + SQL_ARG + " WHERE " + T_FSCACHE_WHERE;
// ===========================================================
// Fields
// ===========================================================
private final Context mCtx;
private final MapTileFilesystemProvider mFSProvider;
private final SQLiteDatabase mDatabase;
private final SQLiteStatement incrementUse;
// ===========================================================
// Constructors
// ===========================================================
public MapTileProviderDataBase(final Context context, MapTileFilesystemProvider openStreetMapTileFilesystemProvider) {
Log.i("OSMTileProviderDB", "creating database instance");
mCtx = context;
mFSProvider = openStreetMapTileFilesystemProvider;
mDatabase = new DatabaseHelper(context).getWritableDatabase();
incrementUse = mDatabase.compileStatement(T_FSCACHE_INCREMENT_USE);
}
public boolean hasTile(final MapTile aTile) {
boolean existed = false;
if (mDatabase.isOpen()) {
final String[] args = new String[]{"" + aTile.rendererID, "" + aTile.zoomLevel, "" + aTile.x, "" + aTile.y};
final Cursor c = mDatabase.query(T_FSCACHE, new String[]{T_FSCACHE_RENDERER_ID}, T_FSCACHE_WHERE, args, null, null, null);
existed = c.getCount() > 0;
c.close();
}
return existed;
}
public boolean isInvalid(final MapTile aTile) {
boolean existed = false;
if (mDatabase.isOpen()) {
final String[] args = new String[]{"" + aTile.rendererID, "" + aTile.zoomLevel, "" + aTile.x, "" + aTile.y};
final Cursor c = mDatabase.query(T_FSCACHE, new String[]{T_FSCACHE_RENDERER_ID}, T_FSCACHE_WHERE_INVALID, args, null, null, null);
existed = c.getCount() > 0;
c.close();
}
return existed;
}
private boolean incrementUse(final MapTile aTile) throws SQLiteFullException, SQLiteDiskIOException {
boolean ret = false;
if (mDatabase.isOpen()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
try {
incrementUse.bindLong(1,System.currentTimeMillis());
incrementUse.bindString(2,aTile.rendererID);
incrementUse.bindLong(3, aTile.zoomLevel);
incrementUse.bindLong(4, aTile.x);
incrementUse.bindLong(5, aTile.y);
return incrementUse.executeUpdateDelete() >= 1; // > 1 is naturally an error, but safe to return true here
} catch (Exception e) {
if (e instanceof SQLiteFullException) {
// database/disk is full
Log.e(MapTileFilesystemProvider.DEBUGTAG, "Tile database full");
Snack.toastTopError(mCtx,R.string.toast_tile_database_full);
throw new SQLiteFullException(e.getMessage());
} else if (e instanceof SQLiteDiskIOException) {
throw new SQLiteDiskIOException(e.getMessage());
}
ACRA.getErrorReporter().putCustomData("STATUS", "NOCRASH");
ACRA.getErrorReporter().handleException(e);
return true; // this will indicate that the tile is in the DB which is erring on the safe side
}
} else {
final String[] args = new String[] { "" + aTile.rendererID, "" + aTile.zoomLevel, "" + aTile.x,
"" + aTile.y };
Cursor c = mDatabase.query(T_FSCACHE, new String[] { T_FSCACHE_USAGECOUNT }, T_FSCACHE_WHERE, args, null,
null, null);
try {
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "incrementUse found " + c.getCount() + " entries");
}
if (c.getCount() == 1) {
try {
c.moveToFirst();
int usageCount = c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_USAGECOUNT));
ContentValues cv = new ContentValues();
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "incrementUse count " + usageCount);
}
cv.put(T_FSCACHE_USAGECOUNT, usageCount + 1);
cv.put(T_FSCACHE_TIMESTAMP, System.currentTimeMillis());
ret = mDatabase.update(T_FSCACHE, cv, T_FSCACHE_WHERE, args) > 0;
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "incrementUse count " + usageCount + " update sucessful " + ret);
}
} catch (Exception e) {
if (e instanceof NullPointerException) {
// just log ... likely these are really spurious
Log.e(MapTileFilesystemProvider.DEBUGTAG, "NPE in incrementUse");
} else if (e instanceof SQLiteFullException) {
// database/disk is full
Log.e(MapTileFilesystemProvider.DEBUGTAG, "Tile database full");
Snack.toastTopError(mCtx,R.string.toast_tile_database_full);
throw new SQLiteFullException(e.getMessage());
} else if (e instanceof SQLiteDiskIOException) {
throw new SQLiteDiskIOException(e.getMessage());
} else {
ACRA.getErrorReporter().putCustomData("STATUS", "NOCRASH");
ACRA.getErrorReporter().handleException(e);
}
}
}
} finally {
c.close();
}
}
} else if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "incrementUse database not open");
}
return ret;
}
public synchronized int addTileOrIncrement(final MapTile aTile, final byte[] tile_data) throws IOException {
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "adding or incrementing use " + aTile);
}
try {
// there seems to be danger for a race condition here
if (incrementUse(aTile)) { // this should actually never be true
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Tile existed");
}
return 0;
} else {
insertNewTile(aTile, tile_data);
return tile_data != null ? tile_data.length : 0;
}
} catch (SQLiteFullException sfex) { // handle these the same
throw new IOException(sfex.getMessage());
} catch (SQLiteDiskIOException sioex) {
throw new IOException(sioex.getMessage());
}
}
private void insertNewTile(final MapTile aTile, final byte[] tile_data) {
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Inserting new tile");
}
if (mDatabase.isOpen()) {
final ContentValues cv = new ContentValues();
cv.put(T_FSCACHE_RENDERER_ID, aTile.rendererID);
cv.put(T_FSCACHE_ZOOM_LEVEL, aTile.zoomLevel);
cv.put(T_FSCACHE_TILE_X, aTile.x);
cv.put(T_FSCACHE_TILE_Y, aTile.y);
cv.put(T_FSCACHE_TIMESTAMP, System.currentTimeMillis());
cv.put(T_FSCACHE_FILESIZE, tile_data != null ? tile_data.length : 0); // 0 == invalid
cv.put(T_FSCACHE_DATA, tile_data);
long result = mDatabase.insert(T_FSCACHE, null, cv);
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Inserting new tile result " + result);
}
}
}
/**
* Returns requested tile and increases use count and date
* @param aTile
* @return the contents of the tile or null on failure to retrieve
* @throws IOException
*/
public synchronized byte[] getTile(final MapTile aTile) throws IOException {
// there seems to be danger for a race condition here
if (DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Trying to retrieve " + aTile + " from file");
}
try {
if (incrementUse(aTile)) { // checks if DB is open
final String[] args = new String[]{"" + aTile.rendererID, "" + aTile.zoomLevel, "" + aTile.x, "" + aTile.y};
final Cursor c = mDatabase.query(T_FSCACHE, new String[] { T_FSCACHE_DATA }, T_FSCACHE_WHERE_NOT_INVALID, args, null, null, null);
try {
if (c.moveToFirst()) {
byte[] tile_data = c.getBlob(c.getColumnIndexOrThrow(T_FSCACHE_DATA));
if (DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Sucessfully retrieved " + aTile + " from file");
}
return tile_data;
} else if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Tile not found but should be 2");
}
} finally {
c.close();
}
}
} catch (SQLiteDiskIOException sioex) { // handle these exceptions the same
throw new IOException(sioex.getMessage());
} catch (SQLiteFullException sdfex) {
throw new IOException(sdfex.getMessage());
}
if(DEBUGMODE) {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Tile not found in DB");
}
return null;
}
synchronized long deleteOldest(final int pSizeNeeded) throws EmptyCacheException {
if (!mDatabase.isOpen()) { // this seems to happen, protect against crashing
Log.e(MapTileFilesystemProvider.DEBUGTAG,"deleteOldest called on closed DB");
return 0;
}
final Cursor c = mDatabase.rawQuery(T_FSCACHE_SELECT_OLDEST, null);
final ArrayList<MapTile> deleteFromDB = new ArrayList<MapTile>();
long sizeGained = 0;
if(c != null){
try {
MapTile tileToBeDeleted;
try {
if (c.moveToFirst()) {
do {
final int sizeItem = c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_FILESIZE));
sizeGained += sizeItem;
tileToBeDeleted = new MapTile(c.getString(c.getColumnIndexOrThrow(T_FSCACHE_RENDERER_ID)),c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_ZOOM_LEVEL)),
c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_TILE_X)),c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_TILE_Y)));
deleteFromDB.add(tileToBeDeleted);
Log.d(DEBUG_TAG,"deleteOldest " + tileToBeDeleted.toString());
} while(c.moveToNext() && sizeGained < pSizeNeeded);
} else {
throw new EmptyCacheException("Cache seems to be empty.");
}
if (mDatabase.isOpen()) {
for (MapTile t : deleteFromDB) {
final String[] args = new String[]{"" + t.rendererID, "" + t.zoomLevel, "" + t.x, "" + t.y};
mDatabase.delete(T_FSCACHE, T_FSCACHE_WHERE, args);
}
}
}
catch (Exception e) {
if (e instanceof NullPointerException) {
// just log ... likely these are really spurious
Log.e(MapTileFilesystemProvider.DEBUGTAG, "NPE in deleteOldest " + e);
} else if (e instanceof SQLiteFullException) {
Log.e(MapTileFilesystemProvider.DEBUGTAG, "Exception in deleteOldest " + e);
Snack.toastTopError(mCtx,R.string.toast_tile_database_full);
} else if (e instanceof SQLiteDiskIOException) {
Log.e(MapTileFilesystemProvider.DEBUGTAG, "Exception in deleteOldest " + e);
} else {
ACRA.getErrorReporter().putCustomData("STATUS","NOCRASH");
ACRA.getErrorReporter().handleException(e);
}
}
} finally {
c.close();
}
}
return sizeGained;
}
/**
* Delete all tiles from cache for a specific renderer
* @param rendererID
* @throws EmptyCacheException
*/
synchronized public void flushCache(String rendererID) throws EmptyCacheException {
Log.d(MapTileFilesystemProvider.DEBUGTAG, "Flushing cache for " + rendererID);
final Cursor c = mDatabase.rawQuery("SELECT " + T_FSCACHE_ZOOM_LEVEL + "," + T_FSCACHE_TILE_X + "," + T_FSCACHE_TILE_Y + "," + T_FSCACHE_FILESIZE + " FROM " + T_FSCACHE + " WHERE " + T_FSCACHE_RENDERER_ID + "='" + rendererID + "' ORDER BY " + T_FSCACHE_TIMESTAMP + " ASC", null);
final ArrayList<MapTile> deleteFromDB = new ArrayList<MapTile>();
long sizeGained = 0;
if(c != null){
try {
MapTile tileToBeDeleted;
if(c.moveToFirst()){
do{
final int sizeItem = c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_FILESIZE));
sizeGained += sizeItem;
tileToBeDeleted = new MapTile(rendererID,c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_ZOOM_LEVEL)),
c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_TILE_X)),c.getInt(c.getColumnIndexOrThrow(T_FSCACHE_TILE_Y)));
deleteFromDB.add(tileToBeDeleted);
// Log.d(DEBUG_TAG,"flushCache " + tileToBeDeleted.toString());
} while (c.moveToNext());
} else {
throw new EmptyCacheException("Cache seems to be empty.");
}
Log.d(DEBUG_TAG,"flushCache freed " + sizeGained);
} finally {
c.close();
}
for(MapTile t : deleteFromDB) {
final String[] args = new String[]{"" + t.rendererID, "" + t.zoomLevel, "" + t.x, "" + t.y};
mDatabase.delete(T_FSCACHE, T_FSCACHE_WHERE, args);
}
}
}
// ===========================================================
// Methods
// ===========================================================
private String TMP_COLUMN = "tmp";
public int getCurrentFSCacheByteSize() {
int ret = 0;
if (mDatabase.isOpen()) {
final Cursor c = mDatabase.rawQuery("SELECT SUM(" + T_FSCACHE_FILESIZE + ") AS " + TMP_COLUMN + " FROM " + T_FSCACHE, null);
if(c != null){
if(c.moveToFirst()){
ret = c.getInt(c.getColumnIndexOrThrow(TMP_COLUMN));
}
c.close();
}
}
return ret;
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
private class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(final Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
try {
db.execSQL(T_RENDERER_CREATE_COMMAND);
db.execSQL(T_FSCACHE_CREATE_COMMAND);
} catch (SQLException e) {
Log.w(MapTileFilesystemProvider.DEBUGTAG, "Problem creating database", e);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if(DEBUGMODE)
Log.w(MapTileFilesystemProvider.DEBUGTAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + T_FSCACHE);
onCreate(db);
}
}
/**
* Close the DB handle
*/
public void close() {
mDatabase.close();
}
/**
* Deletes the database
* @param context
*/
public static void delete(final Context context) {
Log.w(MapTileFilesystemProvider.DEBUGTAG, "Deleting database " + DATABASE_NAME);
context.deleteDatabase(DATABASE_NAME);
}
/**
* Check if the database exists and can be read.
*
* @return true if it exists and can be read and written, false if it doesn't
*/
public static boolean exists(File dir) {
SQLiteDatabase checkDB = null;
try {
String path = dir.getAbsolutePath() + "/databases/" + DATABASE_NAME + ".db";
checkDB = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READWRITE);
checkDB.close();
} catch (Exception e) {
// database doesn't exist yet.
// NOTE this originally caught just SQLiteException however this seems to cause issues with some Android versions
}
return checkDB != null;
}
}