package org.mozilla.osmdroid.tileprovider.modules;
import android.graphics.drawable.Drawable;
import org.mozilla.mozstumbler.service.AppGlobals;
import org.mozilla.mozstumbler.service.core.http.IHttpUtil;
import org.mozilla.mozstumbler.service.core.http.IResponse;
import org.mozilla.mozstumbler.service.core.logging.ClientLog;
import org.mozilla.mozstumbler.svclocator.ServiceLocator;
import org.mozilla.mozstumbler.svclocator.services.ISystemClock;
import org.mozilla.mozstumbler.svclocator.services.log.LoggerUtil;
import org.mozilla.osmdroid.tileprovider.MapTile;
import org.mozilla.osmdroid.tileprovider.tilesource.BitmapTileSourceBase;
import org.mozilla.osmdroid.tileprovider.tilesource.ITileSource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/*
* This is a self contained tile downloader and writer to disk.
*
* Features that this has over the regular MapTileDownloader include:
* - HTTP 404 filtering so that repeated requests that result in a 404 NotFound
* will be cached for one hour.
* - ETag headers are written to disk in a .etag file so that
* condition-get can be implemented using an "If-None-Match" request header
*/
public class TileDownloaderDelegate {
public static final String ETAG_MATCH_HEADER = "If-None-Match";
public static final int ONE_HOUR_MS = 1000 * 60 * 60;
// We use an LRU cache to track any URLs that give us a HTTP 404.
private static final int HTTP404_CACHE_SIZE = 2000;
Map<String, Long> HTTP404_CACHE = Collections.synchronizedMap(new LruCache<String, Long>(HTTP404_CACHE_SIZE));
private static final String LOG_TAG = LoggerUtil.makeLogTag(TileDownloaderDelegate.class);
private final INetworkAvailablityCheck networkAvailablityCheck;
private final TileIOFacade tileIOFacade;
public TileDownloaderDelegate(INetworkAvailablityCheck pNetworkAvailablityCheck,
TileIOFacade tw) {
tileIOFacade = tw;
networkAvailablityCheck = pNetworkAvailablityCheck;
}
/*
* Write a tile from network to disk.
*/
public Drawable downloadTile(SerializableTile serializableTile, ITileSource tileSource, MapTile tile)
throws BitmapTileSourceBase.LowMemoryException {
if (networkIsUnavailable()) {
if (serializableTile.getTileData().length > 0) {
// Just try to return what we've got on disk if the network is just down.
// This should really be pulled out into a wrapping function.
return tileSource.getDrawable(serializableTile.getTileData());
}
return null;
}
ServiceLocator svcLocator = ServiceLocator.getInstance();
if (tileSource == null) {
ClientLog.i(LOG_TAG, "tileSource is null");
return null;
}
final String tileURLString = tileSource.getTileURLString(tile);
if (tileURLString == null || tileURLString.length() == 0) {
return null;
}
if (urlIs404Cached(tileURLString)) {
return null;
}
ISystemClock systemClock = (ISystemClock) svcLocator.getService(ISystemClock.class);
if (systemClock.currentTimeMillis() < serializableTile.getCacheControl()) {
return tileSource.getDrawable(serializableTile.getTileData());
}
// Always try remove the tileURL from the cache before we try
// downloading again.
HTTP404_CACHE.remove(tileURLString);
IHttpUtil httpClient = (IHttpUtil) svcLocator.getService(IHttpUtil.class);
HashMap<String, String> headers = new HashMap<String, String>();
String cachedEtag = serializableTile.getEtag();
if (cachedEtag != null) {
headers.put(ETAG_MATCH_HEADER, cachedEtag);
}
IResponse resp = httpClient.get(tileURLString, headers);
if (AppGlobals.isDebug) {
ClientLog.d(LOG_TAG, "Got a response: " + resp.httpStatusCode());
}
if (resp == null) {
return null;
}
if (resp.httpStatusCode() == 304) {
if (serializableTile.getTileData().length > 0) {
// Resave the file - this will automatically update the cache-control value
serializableTile.saveFile();
return tileSource.getDrawable(serializableTile.getTileData());
} else {
// Something terrible went wrong. Clear the etag and the tile data.
serializableTile.setHeader("etag", "");
serializableTile.setTileData(null);
serializableTile.saveFile();
return null;
}
}
if (resp.httpStatusCode() != 200) {
if (resp.httpStatusCode() == 404) {
HTTP404_CACHE.put(tileURLString, System.currentTimeMillis() + ONE_HOUR_MS);
}
// @TODO vng: This is a hack so that we skip over anything that errors from the mozilla
// cloudfront backed coverage tile server.
if (tileURLString.contains("cloudfront.net")) {
// A refactoring that would be useful is a callback mechanism so that we a TileProvider
// can optionally provide handlers for each HTTP status code to hook logging or other
// behavior.
// Do nothing here for now. We may as well generate an empty bitmap and return that
// on the refactoring.
} else {
ClientLog.w(LOG_TAG, "Error downloading [" + tileURLString + "] HTTP Response Code:" + resp.httpStatusCode());
}
return null;
}
byte[] tileBytes = resp.bodyBytes();
String etag = resp.getFirstHeader("etag");
// write the data using the TileIOFacade
serializableTile = tileIOFacade.saveFile(tileSource, tile, tileBytes, etag);
if (AppGlobals.isDebug) {
ClientLog.d(LOG_TAG, "serializableTile == " + serializableTile);
}
byte[] data = serializableTile.getTileData();
return tileSource.getDrawable(data);
}
/*
* If a networkAvailabilityCheck object exists, check if the
* network is *unavailable* and return true.
*
* In all other cases, assume the network is available.
*/
protected boolean networkIsUnavailable() {
if (networkAvailablityCheck != null && !networkAvailablityCheck.getNetworkAvailable()) {
return true;
}
return false;
}
/*
* Check if this URL is already known to 404 on us.
*/
protected boolean urlIs404Cached(String url) {
Long cacheTs = HTTP404_CACHE.get(url);
if (cacheTs != null) {
if (cacheTs.longValue() > System.currentTimeMillis()) {
return true;
}
}
return false;
}
}