package org.osmdroid.tileprovider.modules;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import org.osmdroid.api.IMapView;
import org.osmdroid.config.Configuration;
import org.osmdroid.tileprovider.MapTile;
import org.osmdroid.tileprovider.constants.OpenStreetMapTileProviderConstants;
import org.osmdroid.tileprovider.tilesource.ITileSource;
import org.osmdroid.tileprovider.util.Counters;
import org.osmdroid.tileprovider.util.StreamUtils;
/**
* An implementation of {@link IFilesystemCache}. It writes tiles to the file system cache. If the
* cache exceeds 600 Mb then it will be trimmed to 500 Mb.
*
* @author Neil Boyd
* @see OpenStreetMapTileProviderConstants
*
*/
public class TileWriter implements IFilesystemCache {
// ===========================================================
// Constants
// ===========================================================
// ===========================================================
// Fields
// ===========================================================
/** amount of disk space used by tile cache **/
private static long mUsedCacheSpace;
static boolean hasInited=false;
Thread initThread=null;
// ===========================================================
// Constructors
// ===========================================================
public TileWriter() {
if (!hasInited) {
hasInited = true;
// do this in the background because it takes a long time
initThread = new Thread() {
@Override
public void run() {
mUsedCacheSpace = 0; // because it's static
calculateDirectorySize(Configuration.getInstance().getOsmdroidTileCache());
if (mUsedCacheSpace > Configuration.getInstance().getTileFileSystemCacheMaxBytes()) {
cutCurrentCache();
}
if (Configuration.getInstance().isDebugMode()) {
Log.d(IMapView.LOGTAG, "Finished init thread");
}
}
};
initThread.setPriority(Thread.MIN_PRIORITY);
initThread.start();
}
}
// ===========================================================
// Getter & Setter
// ===========================================================
/**
* Get the amount of disk space used by the tile cache. This will initially be zero since the
* used space is calculated in the background.
*
* @return size in bytes
*/
public static long getUsedCacheSpace() {
return mUsedCacheSpace;
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
@Override
public boolean saveFile(final ITileSource pTileSource, final MapTile pTile,
final InputStream pStream) {
final File file = new File(Configuration.getInstance().getOsmdroidTileCache(), pTileSource.getTileRelativeFilenameString(pTile)
+ OpenStreetMapTileProviderConstants.TILE_PATH_EXTENSION);
if (Configuration.getInstance().isDebugTileProviders()){
Log.d(IMapView.LOGTAG, "TileWrite " + file.getAbsolutePath());
}
final File parent = file.getParentFile();
if (!parent.exists() && !createFolderAndCheckIfExists(parent)) {
return false;
}
BufferedOutputStream outputStream = null;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(file.getPath()),
StreamUtils.IO_BUFFER_SIZE);
final long length = StreamUtils.copy(pStream, outputStream);
mUsedCacheSpace += length;
if (mUsedCacheSpace > Configuration.getInstance().getTileFileSystemCacheMaxBytes()) {
cutCurrentCache(); // TODO perhaps we should do this in the background
}
} catch (final IOException e) {
Counters.fileCacheSaveErrors++;
return false;
} finally {
if (outputStream != null) {
StreamUtils.closeStream(outputStream);
}
}
return true;
}
@Override
public void onDetach() {
if (initThread!=null){
try {
initThread.interrupt();
}catch (Throwable t){}
}
}
@Override
public boolean remove(final ITileSource pTileSource, final MapTile pTile) {
final File file = new File(Configuration.getInstance().getOsmdroidTileCache(), pTileSource.getTileRelativeFilenameString(pTile)
+ OpenStreetMapTileProviderConstants.TILE_PATH_EXTENSION);
if (file.exists()) {
try {
file.delete();
return true;
}catch (Exception ex){
//potential io exception
Log.i(IMapView.LOGTAG, "Unable to delete cached tile from " + pTileSource.name() + " " + pTile.toString() , ex);
}
}
return false;
}
@Override
public boolean exists(final ITileSource pTileSource, final MapTile pTile) {
return new File(Configuration.getInstance().getOsmdroidTileCache(), pTileSource.getTileRelativeFilenameString(pTile)
+ OpenStreetMapTileProviderConstants.TILE_PATH_EXTENSION).exists();
}
// ===========================================================
// Methods
// ===========================================================
private boolean createFolderAndCheckIfExists(final File pFile) {
if (pFile.mkdirs()) {
return true;
}
if (Configuration.getInstance().isDebugMode()) {
Log.d(IMapView.LOGTAG,"Failed to create " + pFile + " - wait and check again");
}
// if create failed, wait a bit in case another thread created it
try {
Thread.sleep(500);
} catch (final InterruptedException ignore) {
}
// and then check again
if (pFile.exists()) {
if (Configuration.getInstance().isDebugMode()) {
Log.d(IMapView.LOGTAG,"Seems like another thread created " + pFile);
}
return true;
} else {
if (Configuration.getInstance().isDebugMode()) {
Log.d(IMapView.LOGTAG,"File still doesn't exist: " + pFile);
}
return false;
}
}
private void calculateDirectorySize(final File pDirectory) {
final File[] z = pDirectory.listFiles();
if (z != null) {
for (final File file : z) {
if (file.isFile()) {
mUsedCacheSpace += file.length();
}
if (file.isDirectory() && !isSymbolicDirectoryLink(pDirectory, file)) {
calculateDirectorySize(file); // *** recurse ***
}
}
}
}
/**
* Checks to see if it appears that a directory is a symbolic link. It does this by comparing
* the canonical path of the parent directory and the parent directory of the directory's
* canonical path. If they are equal, then they come from the same true parent. If not, then
* pDirectory is a symbolic link. If we get an exception, we err on the side of caution and
* return "true" expecting the calculateDirectorySize to now skip further processing since
* something went goofy.
*/
private boolean isSymbolicDirectoryLink(final File pParentDirectory, final File pDirectory) {
try {
final String canonicalParentPath1 = pParentDirectory.getCanonicalPath();
final String canonicalParentPath2 = pDirectory.getCanonicalFile().getParent();
return !canonicalParentPath1.equals(canonicalParentPath2);
} catch (final IOException e) {
return true;
} catch (final NoSuchElementException e) {
// See: http://code.google.com/p/android/issues/detail?id=4961
// See: http://code.google.com/p/android/issues/detail?id=5807
return true;
}
}
private List<File> getDirectoryFileList(final File aDirectory) {
final List<File> files = new ArrayList<File>();
final File[] z = aDirectory.listFiles();
if (z != null) {
for (final File file : z) {
if (file.isFile()) {
files.add(file);
}
if (file.isDirectory()) {
files.addAll(getDirectoryFileList(file));
}
}
}
return files;
}
/**
* If the cache size is greater than the max then trim it down to the trim level. This method is
* synchronized so that only one thread can run it at a time.
*/
private void cutCurrentCache() {
final File lock=Configuration.getInstance().getOsmdroidTileCache();
synchronized (lock) {
if (mUsedCacheSpace > Configuration.getInstance().getTileFileSystemCacheTrimBytes()) {
Log.d(IMapView.LOGTAG,"Trimming tile cache from " + mUsedCacheSpace + " to "
+ Configuration.getInstance().getTileFileSystemCacheTrimBytes());
final List<File> z = getDirectoryFileList(Configuration.getInstance().getOsmdroidTileCache());
// order list by files day created from old to new
final File[] files = z.toArray(new File[0]);
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(final File f1, final File f2) {
return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
}
});
for (final File file : files) {
if (mUsedCacheSpace <= Configuration.getInstance().getTileFileSystemCacheTrimBytes()) {
break;
}
final long length = file.length();
if (file.delete()) {
if (Configuration.getInstance().isDebugTileProviders()){
Log.d(IMapView.LOGTAG,"Cache trim deleting " + file.getAbsolutePath());
}
mUsedCacheSpace -= length;
}
}
Log.d(IMapView.LOGTAG,"Finished trimming tile cache");
}
}
}
}