package com.androidol.util.tiles; 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.InputStream; import java.io.OutputStream; import java.util.HashSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.util.Log; import com.androidol.constants.UtilConstants; import com.androidol.events.Event; import com.androidol.events.TileEvents; import com.androidol.exceptions.EmptyCacheException; import com.androidol.exceptions.TileFileCorruptedException; import com.androidol.tile.Tile; import com.androidol.util.Util; public class TileFileSystemLoader implements UtilConstants { // =========================================================== // fields // =========================================================== //public static final int LOAD_SUCCESS = 1001; //public static final int LOAD_FAILURE = 1000; protected final Context context; protected final TileFileSystemLoaderDB database; protected final int cacheSizeInByte; protected int cacheUsedInByte; protected ExecutorService threadPool = Executors.newFixedThreadPool(8); protected TileCache tileCache; protected HashSet<String> pendingQueue = new HashSet<String>(); protected TileEvents events = null; /** * Constructor TileFileSystemLoader * * @param context * @param cacheSizeInByte * @param tileCache */ public TileFileSystemLoader(Context context, final int cacheSizeInByte, final TileCache tileCache, TileEvents tileEvents) { this.context = context; this.cacheSizeInByte = cacheSizeInByte; this.tileCache = tileCache; this.database = new TileFileSystemLoaderDB(context); this.cacheUsedInByte = this.database.getCacheUsedInByte(); this.events = tileEvents; //Util.printDebugMessage("...cache size on disk: " + this.cacheSizeInByte + " bytes" + " | " + "used cache: " + this.cacheUsedInByte + " bytes..."); } /** * API Method: getCacheUsedInByte * * @return * return used cache size */ public int getCacheUsedInByte() { return this.cacheUsedInByte; } /** * * @param url * @param tile * @return * @throws FileNotFoundException * @throws TileFileCorruptedException */ public Bitmap loadTileToMemorySync(final String url) throws FileNotFoundException, TileFileCorruptedException { if(this.pendingQueue.contains(url)) { //Util.printDebugMessage("...tile " + url + " already in the queue...skip loading from disk...."); this.pendingQueue.remove(url); } final String formattedUrlString = formatTileUrlToTileFilePath(url); FileInputStream fileInputStream = this.context.openFileInput(formattedUrlString); // determine whether the tile file on FS is corrupted or not try { if(fileInputStream.available() == 0) { Util.printWarningMessage(" ...tile " + url + " may be corrupted on disk..."); //this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url)); throw new TileFileCorruptedException(" ...tile " + url + " may be corrupted on disk..."); } } catch(IOException e) { throw new TileFileCorruptedException(e.getMessage()); } final InputStream in = new BufferedInputStream(fileInputStream, IO_BUFFER_SIZE); synchronized(this) { this.pendingQueue.add(url); } OutputStream out = null; Bitmap bitmap = null; try { TileFileSystemLoader.this.database.incrementUse(formattedUrlString); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); out = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); StreamUtils.copy(in, out); out.flush(); final byte[] data = dataStream.toByteArray(); //Util.printDebugMessage("...bitmap size in bytes: " + data.length + "..."); if(data.length <= 0) { Util.printWarningMessage("...load tile " + url + " from disk failed...file corrupted..."); //TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url)); TileFileSystemLoader.this.tileCache.removeTile(url); TileFileSystemLoader.this.database.removeTile(formatTileUrlToTileFilePath(url)); // TODO: update the this.cacheUsedInByte throw new Exception("...load tile " + url + " from disk failed...file corrupted..."); } bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); TileFileSystemLoader.this.pendingQueue.remove(url); TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_LOAD_SUCCESS, new Event(TileEvents.FS_LOAD_SUCCESS, url)); //Util.printDebugMessage(" ...image size: " + data.length + " bytes"); } catch (Exception e) { Util.printErrorMessage("...error loading tile from disk...exception: " + e.getClass().getSimpleName() + "...", e); TileFileSystemLoader.this.pendingQueue.remove(url); } finally { StreamUtils.closeStream(in); StreamUtils.closeStream(out); TileFileSystemLoader.this.pendingQueue.remove(url); } return bitmap; } /** * API Method: loadTileToMemoryAsync * * @param url * @param callback * @throws FileNotFoundException */ public void loadTileToMemoryAsync(final String url, final Tile tile) throws FileNotFoundException, TileFileCorruptedException { if(this.pendingQueue.contains(url)) { //Util.printDebugMessage("...tile " + url + " already in the queue...skip loading from disk...."); // TODO: should I kill the previous loading thread and start a new one? return; } // cached tiles are stored in context final String formattedUrlString = formatTileUrlToTileFilePath(url); FileInputStream fileInputStream = this.context.openFileInput(formattedUrlString); // determine whether the tile file on FS is corrupted or not try { if(fileInputStream.available() == 0) { Util.printWarningMessage(" ...tile " + url + " may be corrupted on disk..."); //this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url)); throw new TileFileCorruptedException(" ...tile " + url + " may be corrupted on disk..."); } } catch(IOException e) { throw new TileFileCorruptedException(e.getMessage()); } finally { // TODO: delete this entry from disk // so that the follow on http loader will get it again and fill the gap } final InputStream in = new BufferedInputStream(fileInputStream, IO_BUFFER_SIZE); synchronized(this) { this.pendingQueue.add(url); } //Util.printDebugMessage(" ...tile " + url + " added in TileFileSystemLoader pending queue..."); //Util.printDebugMessage(" ...TileFileSystemLoader pending queue size: " + this.pendingQueue.size()); this.threadPool.execute( new Runnable() { @Override public void run() { //Util.printDebugMessage("@...started a thread for fs loading...url: " + url); OutputStream out = null; try { TileFileSystemLoader.this.database.incrementUse(formattedUrlString); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); out = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); StreamUtils.copy(in, out); out.flush(); final byte[] data = dataStream.toByteArray(); //Util.printDebugMessage("...bitmap size in bytes: " + data.length + "..."); if(data.length <= 0) { Util.printWarningMessage("...load tile " + url + " from disk failed...file corrupted..."); //TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url)); TileFileSystemLoader.this.tileCache.removeTile(url); TileFileSystemLoader.this.database.removeTile(formatTileUrlToTileFilePath(url)); // TODO: update the this.cacheUsedInByte throw new Exception("...load tile " + url + " from disk failed...file corrupted..."); } final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); //Util.printDebugMessage(" ...image size: " + data.length + " bytes"); TileFileSystemLoader.this.tileCache.putTile(url, bitmap, tile); //TileFileSystemLoader.this.pendingQueue.remove(url); //Util.printDebugMessage("...fs loader pending queue size: " + TileFileSystemLoader.this.pendingQueue.size()); TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_LOAD_SUCCESS, new Event(TileEvents.FS_LOAD_SUCCESS, url)); } catch(InterruptedException e) { // when thread is blocked, interrupt() call throws exception // TODO: thread interrupted // } catch (Exception e) { TileFileSystemLoader.this.pendingQueue.remove(url); //TileFileSystemLoader.this.events.triggerEvent(TileEvents.FS_TILE_CORRUPTED, new Event(TileEvents.FS_TILE_CORRUPTED, url)); Util.printErrorMessage("...error loading tile from disk...exception: " + e.getClass().getSimpleName() + "...", e); } finally { StreamUtils.closeStream(in); StreamUtils.closeStream(out); TileFileSystemLoader.this.pendingQueue.remove(url); //Util.printDebugMessage("...finish a thread for fs loading..."); //Util.printDebugMessage("@...finish a thread for fs loading...url: " + url); } //Util.printDebugMessage(" ...tile " + url + " removed from TileFileSystemLoader pending queue...queue size: " + TileFileSystemLoader.this.pendingQueue.size()); //Util.printDebugMessage(" ...TileFileSystemLoader pending queue size: " + TileFileSystemLoader.this.pendingQueue.size()); } }); } /** * API Method: saveFile * * @param url * @param data * @throws IOException */ public void saveFile(final String url, final byte[] data) throws IOException { // store cached tiles in context final String fileName = formatTileUrlToTileFilePath(url); final FileOutputStream fos = this.context.openFileOutput(fileName, Context.MODE_WORLD_READABLE); final BufferedOutputStream bos = new BufferedOutputStream(fos, StreamUtils.IO_BUFFER_SIZE); bos.write(data); bos.flush(); bos.close(); synchronized(this) { final int bytesGrown = this.database.addTileOrIncrement(fileName, data.length); this.cacheUsedInByte += bytesGrown; //Util.printDebugMessage("used cache is now: " + this.cacheUsedInByte + " bytes..."); try { if(this.cacheUsedInByte > this.cacheSizeInByte){ //Util.printDebugMessage(" ...used cache exceeds max cache size...free cache..."); this.cacheUsedInByte -= this.database.deleteOldest((int)(this.cacheSizeInByte * 0.05f)); // Free 5% of cache } //printCurrentFileSystemCacheStatus(); } catch(EmptyCacheException e) { Util.printErrorMessage("...cache empty...", e); } } } /** * API Method: cleanupCache */ public void cleanupCache(){ cleanupCacheBy(Integer.MAX_VALUE); // Delete all } /** * API Method: cleanupCacheBy * * @param bytesToCut */ public void cleanupCacheBy(final int bytesToCut){ try { this.database.deleteOldest(bytesToCut); this.cacheUsedInByte = 0; //printCurrentFileSystemCacheStatus(); } catch(EmptyCacheException e) { Util.printErrorMessage(" ...cache empty...", e); } } /** * * @param url * @return */ protected String formatTileUrlToTileFilePath(String url) { return url.substring(7).replace("/", "_"); } /** * */ public void printCurrentFileSystemCacheStatus() { Log.i(DEBUGTAG, " //============================================================================="); Log.i(DEBUGTAG, " //...total file system cache size (in bytes): " + this.cacheSizeInByte); Log.i(DEBUGTAG, " //...used file system cache size (in bytes): " + this.cacheUsedInByte); Log.i(DEBUGTAG, " //============================================================================="); } }