/*
This file is part of OpenSatNav.
OpenSatNav 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 3 of the License, or
(at your option) any later version.
OpenSatNav 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 OpenSatNav. If not, see <http://www.gnu.org/licenses/>.
*/
// Created by plusminus on 21:46:41 - 25.09.2008
package org.andnav.osm.views.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.andnav.osm.exceptions.EmptyCacheException;
import org.andnav.osm.util.constants.OpenStreetMapConstants;
import org.andnav.osm.views.util.constants.OpenStreetMapViewConstants;
import org.opensatnav.OpenSatNavConstants;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
*
* @author Nicolas Gramlich
*
*/
public class OpenStreetMapTileFilesystemProvider implements
OpenStreetMapConstants, OpenStreetMapViewConstants {
// ===========================================================
// Constants
// ===========================================================
public static final int MAPTILEFSLOADER_SUCCESS_ID = 1000;
public static final int MAPTILEFSLOADER_FAIL_ID = MAPTILEFSLOADER_SUCCESS_ID + 1;
private static final int WEEK_MILLISECONDS = 1000 * 60 * 60 * 24 * 7;
protected static final SimpleDateFormat DATE_FORMAT_ISO8601 = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS");
// public static final Options BITMAPLOADOPTIONS = new Options(){
// {
// inPreferredConfig = Config.RGB_565;
// }
// };
// ===========================================================
// Fields
// ===========================================================
protected final Context mCtx;
protected final OpenStreetMapTileFilesystemProviderDataBase mDatabase;
protected int mMaxFSCacheByteSize;
protected int mCurrentFSCacheByteSize;
protected ExecutorService mThreadPool = Executors.newFixedThreadPool(2);
protected final OpenStreetMapTileCache mCache;
protected OpenStreetMapTileDownloader mDownloader;
protected final String tileFolder = OpenSatNavConstants.TILE_CACHE_PATH;
protected Set<String> mPending = Collections
.synchronizedSet(new HashSet<String>());
// ===========================================================
// Constructors
// ===========================================================
/**
* @param ctx
* @param aMaxFSCacheByteSize
* the size of the cached MapTiles will not exceed this size.
* @param aCache
* to load fs-tiles to.
*/
public OpenStreetMapTileFilesystemProvider(final Context ctx,
final int aMaxFSCacheByteSize, final OpenStreetMapTileCache aCache) {
this.mCtx = ctx;
this.mMaxFSCacheByteSize = aMaxFSCacheByteSize;
this.mDatabase = new OpenStreetMapTileFilesystemProviderDataBase(ctx);
this.mCurrentFSCacheByteSize = this.mDatabase
.getCurrentFSCacheByteSize();
this.mCache = aCache;
if (DEBUGMODE)
Log.i(DEBUGTAG, "Currently used cache-size is: "
+ this.mCurrentFSCacheByteSize + " of "
+ this.mMaxFSCacheByteSize + " Bytes");
}
// ===========================================================
// Getter & Setter
// ===========================================================
public void setTileDownloader(OpenStreetMapTileDownloader aDownloader) {
this.mDownloader = aDownloader;
}
public int getCurrentFSCacheByteSize() {
return this.mCurrentFSCacheByteSize;
}
public void setCurrentFSCacheByteSize(int newSize) {
if (newSize > mMaxFSCacheByteSize)
this.mMaxFSCacheByteSize = newSize;
else if (newSize < mMaxFSCacheByteSize) {
cutCurrentFSCacheBy(mMaxFSCacheByteSize - newSize);
mMaxFSCacheByteSize = newSize;
}
}
public File getFileForURL(final String aTileURLString) {
return new File(OpenSatNavConstants.DATA_ROOT_DEVICE, tileFolder
+ File.separator + aTileURLString.substring(7) + ".osn");
}
public void loadMapTileToMemCacheAsync(final String aTileURLString,
final Handler callback) throws FileNotFoundException {
if (this.mPending.contains(aTileURLString))
return;
final String formattedTileURLString = OpenStreetMapTileNameFormatter
.format(aTileURLString);
final File tileFile = getFileForURL(aTileURLString);
FileInputStream fis = null;
final boolean onSD = tileFile.canRead();
final String tileEntryURL = (onSD?tileFile.getAbsolutePath():formattedTileURLString);
if (DEBUGMODE)
Log.d(DEBUGTAG, "Requesting tile " + tileEntryURL + (onSD?" from SD Card" : " from internal memory"));
if (onSD) {
fis = new FileInputStream(tileFile);
} else {
fis = this.mCtx.openFileInput(formattedTileURLString);
}
final BufferedInputStream bis = new BufferedInputStream(fis, 4096);
this.mPending.add(aTileURLString);
this.mThreadPool.execute(new Runnable() {
@Override
public void run() {
OutputStream out = null;
try {
// File exists, otherwise a FileNotFoundException would have
// been thrown
OpenStreetMapTileFilesystemProvider.this.mDatabase
.incrementUse(tileEntryURL, false, null);
final ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
out = new BufferedOutputStream(dataStream,
StreamUtils.IO_BUFFER_SIZE);
StreamUtils.copy(bis, out);
out.flush();
final byte[] data = dataStream.toByteArray();
final Bitmap bmp = BitmapFactory.decodeByteArray(data, 0,
data.length); // ,
// BITMAPLOADOPTIONS);
if (bmp == null) {
// image file is obviously corrupted - remove it.
if (onSD) {
tileFile.delete();
} else {
OpenStreetMapTileFilesystemProvider.this.mCtx
.deleteFile(formattedTileURLString);
}
throw new IOException("Image file was not decodable: "
+ tileFile);
}
OpenStreetMapTileFilesystemProvider.this.mCache.putTile(
aTileURLString, bmp);
final Message successMessage = Message.obtain(callback,
MAPTILEFSLOADER_SUCCESS_ID);
successMessage.sendToTarget();
TileMetaData metadata = OpenStreetMapTileFilesystemProvider.this.mDatabase.queryTileMetaData(tileEntryURL);
Date dateAdded = (metadata != null ? metadata.getDateAdded() : null);
if (dateAdded == null || System.currentTimeMillis() - dateAdded.getTime() > WEEK_MILLISECONDS) {
if (DEBUGMODE) {
String lastSeen = dateAdded == null?"never":DATE_FORMAT_ISO8601.format(dateAdded);
Log.d(DEBUGTAG, "Tile "+ aTileURLString +" last seen " + lastSeen + ", checking with server");
}
mDownloader.requestMapTileAsync(aTileURLString, callback, metadata);
} else if (DEBUGMODE) {
Log.d(DEBUGTAG, "Tile "+ aTileURLString +" last seen " + DATE_FORMAT_ISO8601.format(dateAdded) + ", no server check");
}
} catch (IOException e) {
final Message failMessage = Message.obtain(callback,
MAPTILEFSLOADER_FAIL_ID);
failMessage.sendToTarget();
if (DEBUGMODE)
Log.e(DEBUGTAG,
"Error Loading MapTile from FS. Exception: "
+ e.getClass().getSimpleName(), e);
} finally {
StreamUtils.closeStream(bis);
StreamUtils.closeStream(out);
}
OpenStreetMapTileFilesystemProvider.this.mPending.remove(aTileURLString);
}
});
}
public void saveFile(final String aTileURLString, final TileMetaData metadata, final byte[] someData)
throws IOException {
if (someData.length == 0) {
throw new IOException("Cannot save file of zero length: "
+ aTileURLString);
}
final String formattedTileURLString = OpenStreetMapTileNameFormatter
.format(aTileURLString);
final File tileFile = getFileForURL(aTileURLString);
final File folderPath = tileFile.getParentFile();
String chosenPath;
FileOutputStream fos = null;
if (OpenSatNavConstants.DATA_ROOT_DEVICE.canWrite()) {
folderPath.mkdirs();
fos = new FileOutputStream(tileFile);
chosenPath = tileFile.getAbsolutePath();
} else {
fos = this.mCtx.openFileOutput(formattedTileURLString,
Context.MODE_WORLD_READABLE);
chosenPath = formattedTileURLString;
}
final BufferedOutputStream bos = new BufferedOutputStream(fos, 4096);
bos.write(someData);
bos.flush();
bos.close();
synchronized (this) {
final int bytesGrown = this.mDatabase.addTileOrIncrement(
chosenPath, someData.length, metadata);
this.mCurrentFSCacheByteSize += bytesGrown;
if (DEBUGMODE)
Log.i(DEBUGTAG, "FSCache Size is now: "
+ this.mCurrentFSCacheByteSize + " Bytes");
/* If Cache is full... */
try {
if (this.mCurrentFSCacheByteSize > this.mMaxFSCacheByteSize) {
if (DEBUGMODE)
Log.d(DEBUGTAG, "Freeing FS cache...");
this.mCurrentFSCacheByteSize -= this.mDatabase
.deleteOldest((int) (this.mMaxFSCacheByteSize * 0.05f)); // Free
// 5%
// of
// cache
}
} catch (EmptyCacheException e) {
if (DEBUGMODE)
Log.e(DEBUGTAG, "Cache empty", e);
}
}
}
public void updateFile(final String aTileURLString) {
final String formattedTileURLString = OpenStreetMapTileNameFormatter
.format(aTileURLString);
this.mDatabase.incrementUse(formattedTileURLString, true, null);
}
public void clearCurrentFSCache() {
cutCurrentFSCacheBy(Integer.MAX_VALUE); // Delete all
}
public void cutCurrentFSCacheBy(final int bytesToCut) {
try {
this.mDatabase.deleteOldest(bytesToCut);
} catch (EmptyCacheException e) {
if (DEBUGMODE)
Log.e(DEBUGTAG, "Cache empty", e);
}
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
// ===========================================================
// Methods
// ===========================================================
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
private interface OpenStreetMapTileFilesystemProviderDataBaseConstants {
public static final String DATABASE_NAME = "osmaptilefscache_db";
public static final int DATABASE_VERSION = 5;
public static final String T_FSCACHE = "t_fscache";
public static final String T_FSCACHE_NAME = "name_id";
public static final String T_FSCACHE_U_TIMESTAMP = "used_timestamp";
public static final String T_FSCACHE_A_TIMESTAMP = "added_timestamp";
public static final String T_FSCACHE_ETAG = "etag";
public static final String T_FSCACHE_USAGECOUNT = "countused";
public static final String T_FSCACHE_FILESIZE = "filesize";
public static final String T_FSCACHE_CREATE_COMMAND = "CREATE TABLE IF NOT EXISTS "
+ T_FSCACHE
+ " ("
+ T_FSCACHE_NAME
+ " VARCHAR(255),"
+ T_FSCACHE_A_TIMESTAMP
+ " DATE NOT NULL,"
+ T_FSCACHE_U_TIMESTAMP
+ " DATE NOT NULL,"
+ T_FSCACHE_ETAG
+ " VARCHAR(255),"
+ T_FSCACHE_USAGECOUNT
+ " INTEGER NOT NULL DEFAULT 1,"
+ T_FSCACHE_FILESIZE
+ " INTEGER NOT NULL,"
+ " PRIMARY KEY("
+ T_FSCACHE_NAME + ")" + ");"; // "
public static final String T_FSCACHE_SELECT_LEAST_USED = "SELECT "
+ T_FSCACHE_NAME + "," + T_FSCACHE_FILESIZE + " FROM "
+ T_FSCACHE + " WHERE " + T_FSCACHE_USAGECOUNT
+ " = (SELECT MIN(" + T_FSCACHE_USAGECOUNT + ") FROM "
+ T_FSCACHE + ")";
public static final String T_FSCACHE_SELECT_OLDEST = "SELECT "
+ T_FSCACHE_NAME + "," + T_FSCACHE_FILESIZE + " FROM "
+ T_FSCACHE + " ORDER BY " + T_FSCACHE_U_TIMESTAMP + " ASC";
}
private class OpenStreetMapTileFilesystemProviderDataBase implements
OpenStreetMapTileFilesystemProviderDataBaseConstants,
OpenStreetMapViewConstants {
// ===========================================================
// Fields
// ===========================================================
protected final Context mCtx;
protected final SQLiteDatabase mDatabase;
// ===========================================================
// Constructors
// ===========================================================
public OpenStreetMapTileFilesystemProviderDataBase(final Context context) {
this.mCtx = context;
this.mDatabase = new AndNavDatabaseHelper(context)
.getWritableDatabase();
}
@Override
protected void finalize() throws Throwable {
super.finalize();
// ensure the SqlLite db is closed
// TODO : doesn't seem to do anything
if (this.mDatabase != null)
this.mDatabase.close();
}
public void incrementUse(final String aFormattedTileURLString,
boolean update_added_timestamp, final TileMetaData metadata) {
StringBuilder query = new StringBuilder("UPDATE ").append(T_FSCACHE).append(" SET ")
.append(T_FSCACHE_USAGECOUNT).append(" = ").append(T_FSCACHE_USAGECOUNT).append(" + 1 , ")
.append(T_FSCACHE_U_TIMESTAMP).append(" = datetime('now')");
if (update_added_timestamp)
query.append(", ").append(T_FSCACHE_A_TIMESTAMP).append(" = datetime('now')");
if (metadata != null)
query.append(", ").append(T_FSCACHE_ETAG).append(" = '").append(metadata.getEtag()).append("'");
this.mDatabase.execSQL(query.append(" WHERE ").append(T_FSCACHE_NAME).append(" = '").append(aFormattedTileURLString).append("'").toString());
}
public int addTileOrIncrement(final String tileURLString,
final int aByteFilesize, TileMetaData metadata) {
final Cursor c = this.mDatabase.rawQuery("SELECT * FROM "
+ T_FSCACHE + " WHERE " + T_FSCACHE_NAME + " = '"
+ tileURLString + "'", null);
final boolean existed = c.getCount() > 0;
c.close();
if (DEBUGMODE)
Log.d(DEBUGTAG, "Tile existed=" + existed);
if (existed) {
incrementUse(tileURLString, true, metadata);
return 0;
} else {
insertNewTileInfo(tileURLString, aByteFilesize, metadata);
return aByteFilesize;
}
}
public TileMetaData queryTileMetaData(final String aFormattedTileURLString) {
// TODO : replace this query by a parametrable one (see rawQuery(sql, params))
final Cursor c = this.mDatabase.rawQuery("SELECT "
+ T_FSCACHE_A_TIMESTAMP + ", " + T_FSCACHE_ETAG + " FROM " + T_FSCACHE + " WHERE "
+ T_FSCACHE_NAME + " = '" + aFormattedTileURLString + "'",
null);
TileMetaData metadata = null;
if (c != null) {
if (c.moveToFirst()) {
metadata = new TileMetaData(parseDate(c.getString(c
.getColumnIndexOrThrow(T_FSCACHE_A_TIMESTAMP))), c.getString(c
.getColumnIndexOrThrow(T_FSCACHE_ETAG)));
if (DEBUGMODE)
Log.d(DEBUGTAG, "Etag/Date found for " + aFormattedTileURLString + " : " + metadata.getEtag() + " / " + metadata.getDateAdded());
}
c.close();
}
return metadata;
}
private Date parseDate(String dateString) {
Date result;
try {
result = DATE_FORMAT_ISO8601.parse(dateString);
} catch (java.text.ParseException e) {
result = null;
}
return result;
}
private void insertNewTileInfo(final String aFormattedTileURLString,
final int aByteFilesize, final TileMetaData metadata) {
final ContentValues cv = new ContentValues();
cv.put(T_FSCACHE_NAME, aFormattedTileURLString);
cv.put(T_FSCACHE_A_TIMESTAMP, getNowAsIso8601());
cv.put(T_FSCACHE_U_TIMESTAMP, getNowAsIso8601());
cv.put(T_FSCACHE_ETAG, metadata.getEtag());
cv.put(T_FSCACHE_FILESIZE, aByteFilesize);
this.mDatabase.insert(T_FSCACHE, null, cv);
}
private int deleteOldest(final int pSizeNeeded)
throws EmptyCacheException {
final Cursor c = this.mDatabase.rawQuery(T_FSCACHE_SELECT_OLDEST,
null);
final ArrayList<String> deleteFromDB = new ArrayList<String>();
int sizeGained = 0;
if (c != null) {
String fileNameOfDeleted;
if (c.moveToFirst()) {
do {
final int sizeItem = c.getInt(c
.getColumnIndexOrThrow(T_FSCACHE_FILESIZE));
sizeGained += sizeItem;
fileNameOfDeleted = c.getString(c
.getColumnIndexOrThrow(T_FSCACHE_NAME));
// if the file is in the internal memory it will not
// have a slash in it
// so this is how we test where we should delete from
boolean success = false;
if (fileNameOfDeleted.indexOf("/") == -1)
success = this.mCtx.deleteFile(fileNameOfDeleted);
else
success = new File(fileNameOfDeleted).delete();
if (success)
deleteFromDB.add(fileNameOfDeleted);
if (DEBUGMODE)
Log.i(DEBUGTAG, "Deleted from FS: "
+ fileNameOfDeleted + " for " + sizeItem
+ " Bytes");
} while (c.moveToNext() && sizeGained < pSizeNeeded);
} else {
c.close();
throw new EmptyCacheException("Cache seems to be empty.");
}
c.close();
for (String fn : deleteFromDB)
this.mDatabase.delete(T_FSCACHE, T_FSCACHE_NAME + "='" + fn
+ "'", null);
}
return sizeGained;
}
// ===========================================================
// Methods
// ===========================================================
private String TMP_COLUMN = "tmp";
public int getCurrentFSCacheByteSize() {
final Cursor c = this.mDatabase.rawQuery("SELECT SUM("
+ T_FSCACHE_FILESIZE + ") AS " + TMP_COLUMN + " FROM "
+ T_FSCACHE, null);
final int ret;
if (c != null) {
if (c.moveToFirst()) {
ret = c.getInt(c.getColumnIndexOrThrow(TMP_COLUMN));
} else {
ret = 0;
}
} else {
ret = 0;
}
c.close();
return ret;
}
/**
* Get at the moment within ISO8601 format.
*
* @return Date and time in ISO8601 format.
*/
private String getNowAsIso8601() {
return DATE_FORMAT_ISO8601.format(new Date(System
.currentTimeMillis()));
}
// ===========================================================
// Inner and Anonymous Classes
// ===========================================================
private class AndNavDatabaseHelper extends SQLiteOpenHelper {
AndNavDatabaseHelper(final Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(T_FSCACHE_CREATE_COMMAND);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
if (DEBUGMODE)
Log.w(DEBUGTAG, "Upgrading database from version "
+ oldVersion + " to " + newVersion
+ ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + T_FSCACHE);
onCreate(db);
}
}
}
public final static class TileMetaData {
private final String etag;
private final Date dateAdded;
public TileMetaData(Date dateAdded, String etag) {
this.dateAdded = dateAdded;
this.etag = etag;
}
public TileMetaData(String etag) {
this(null, etag);
}
public String getEtag() {
return etag;
}
public Date getDateAdded() {
return dateAdded;
}
}
}