package org.mozilla.osmdroid.tileprovider.modules;
import org.mozilla.mozstumbler.service.AppGlobals;
import org.mozilla.mozstumbler.service.core.logging.ClientLog;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import org.mozilla.osmdroid.tileprovider.MapTile;
import org.mozilla.osmdroid.tileprovider.constants.OSMConstants;
import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
/*
This is also not a cache at all. It's the only place in osmdroid that
handles disk writes.
Reads are generally handled by the BitmapTileSourceBase class.
The class has been extended to do read/write of etag and cache-control.
*/
public class TileIOFacade {
// ===========================================================
// Constants
// ===========================================================
private static final String LOG_TAG = LoggerUtil.makeLogTag(TileIOFacade.class);
// ===========================================================
// Fields
// ===========================================================
/**
* amount of disk space used by tile cache *
*/
private static long mUsedCacheSpace;
// ===========================================================
// Constructors
// ===========================================================
public TileIOFacade() {
shrinkCacheInBackground();
}
/**
* 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;
}
// ===========================================================
// Getter & Setter
// ===========================================================
private void shrinkCacheInBackground() {
// @TODO: vng put a static synchronized guard here so that the
// background shrink can only happen in 1 background thread at
// a time.
//
// We can then invoke the shrink method as much as we want
// without worrying about spinning up too many background
// threads.
final Thread t = new Thread() {
@Override
public void run() {
mUsedCacheSpace = 0; // because it's static
calculateDirectorySize(OSMConstants.TILE_PATH_BASE);
if (mUsedCacheSpace > OSMConstants.TILE_MAX_CACHE_SIZE_BYTES) {
cutCurrentCache();
}
}
};
t.setPriority(Thread.MIN_PRIORITY);
t.start();
}
// ===========================================================
// Methods from SuperClass/Interfaces
// ===========================================================
// @TODO vng: this should really just take in a header defined as
// Map<String, String> instead of the single etag header
public SerializableTile saveFile(final ITileSource pTileSource, final MapTile pTile,
final byte[] tileBytes, String etag) {
File parent;
File sTileFile = new File(OSMConstants.TILE_PATH_BASE,
pTileSource.getTileRelativeFilenameString(pTile) + OSMConstants.MERGED_FILE_EXT);
parent = sTileFile.getParentFile();
if (!parent.exists() && !createFolderAndCheckIfExists(parent)) {
ClientLog.w(LOG_TAG, "Can't create parent folder for actual serializable tile. parent [" + parent + "]");
return null;
}
SerializableTile serializableTile = new SerializableTile(tileBytes, etag);
serializableTile.saveFile(sTileFile);
mUsedCacheSpace += tileBytes.length;
if (mUsedCacheSpace > OSMConstants.TILE_MAX_CACHE_SIZE_BYTES) {
cutCurrentCache(); // TODO perhaps we should do this in the background
}
return serializableTile;
}
// ===========================================================
// Methods
// ===========================================================
private boolean createFolderAndCheckIfExists(final File pFile) {
if (pFile.mkdirs()) {
return true;
}
if (AppGlobals.isDebug) {
ClientLog.d(LOG_TAG, "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 (AppGlobals.isDebug) {
ClientLog.d(LOG_TAG, "Seems like another thread created " + pFile);
}
return true;
} else {
if (AppGlobals.isDebug) {
ClientLog.d(LOG_TAG, "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() {
synchronized (OSMConstants.TILE_PATH_BASE) {
if (mUsedCacheSpace > OSMConstants.TILE_TRIM_CACHE_SIZE_BYTES) {
ClientLog.i(LOG_TAG, "Trimming tile cache from " + mUsedCacheSpace + " to "
+ OSMConstants.TILE_TRIM_CACHE_SIZE_BYTES);
final List<File> z = getDirectoryFileList(OSMConstants.TILE_PATH_BASE);
// 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 <= OSMConstants.TILE_TRIM_CACHE_SIZE_BYTES) {
break;
}
final long length = file.length();
if (file.delete()) {
mUsedCacheSpace -= length;
}
}
ClientLog.i(LOG_TAG, "Finished trimming tile cache");
}
}
}
}