package com.rareventure.gps2.reviewer.map;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;
import com.mapzen.tangram.HttpHandler;
import com.rareventure.android.SuperThread;
import com.rareventure.gps2.GTG;
import okhttp3.Cache;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.Util;
import okhttp3.internal.io.FileSystem;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import okhttp3.Response;
import okio.BufferedSource;
/**
* This http handler alters caching somewhat, it assumes a slow
* connection. We cache every tile, and
* whenever a request comes in, we try to satisfy it with our cache.
* If not found, we go to the network, and return it, while saving the
* result in our cache also.
* <p>
* If found, then we check the age of the tile. If it's really out of
* date, (on the order of months), we will check the network for a replacement.
* If the network fails, we return the cache.
* (The goal here is to minimize making the user wait for the network, but also not
* return ancient tiles unless absolutely necessary).
* <p>
* If the age of the tile is not very fresh, but not ancient enough to
* check the network first, we return the cached tile as a result, and then go
* out to the network to replace our cached tile afterwards.
* This keeps the app responsive, but also loads new tiles in a reasonable
* timeframe.
* </p>
* <p>
* Since it's impossible to get tangram to reload a tile currently, we
* can't update it with another tile after we've returned one. This is
* why we need to choose whether a cached tile will work, even though it
* may not be the freshest copy.
* </p>
*/
public class GpsTrailerMapzenHttpHandler extends HttpHandler
{
/*
private static final long MAX_TILE_LENGTH = 1024*1024*10;
private static Preferences prefs = new Preferences();
private final File cacheDir;
private SuperThread superThread;
public GpsTrailerMapzenHttpHandler(File cacheDir, SuperThread superThread) {
super();
this.cacheDir = cacheDir;
this.superThread = superThread;
}
@Override
public boolean onRequest(String url, Callback cb) {
superThread.addTask(new RequestTask(url, cb));
return true;
}
private static enum NetworkState
{ NOT_TRIED, SUCCESS, FAIL };
private static enum CacheState
{
NOT_TRIED,
ANCIENT, //cache file is there, but very old
OK, //cache file is there, but is somewhat old
FRESH, //cache file is present and piping hot
CORRUPT_OR_MISSING //cache file is either unreadble or missing
};
private class RequestTask extends SuperThread.Task
{
private static final long MIN_TILE_LENGTH = 1;
private final String url;
private final File file;
private final Callback myCb;
private final Callback cb;
private byte [] responseData;
private boolean respondedToCallback = false;
private NetworkState ns = NetworkState.NOT_TRIED;
private CacheState cs = CacheState.NOT_TRIED;
private boolean savedNetworkResponseToCache = false;
private IOException networkFailException;
private Call networkFailCall;
@Override
public String toString() {
return "RequestTask{" +
"file=" + file +
", ns=" + ns +
", cs=" + cs +
", respondedToCallback=" + respondedToCallback +
", savedNetworkResponseToCache=" + savedNetworkResponseToCache +
", url='" + url + '\'' +
'}';
}
RequestTask(String url, Callback cb)
{
super(0);
this.url = url;
this.cb = cb;
this.myCb = new Callback() {
@Override
public void onFailure(Call c, IOException e) {
RequestTask.this.ns = NetworkState.FAIL;
RequestTask.super.stNotify(RequestTask.this);
networkFailCall = c;
networkFailException = e;
}
@Override
public void onResponse(Call c, Response response) {
if(!response.isSuccessful()) {
RequestTask.this.ns = NetworkState.FAIL;
}
else
{
try {
RequestTask.this.responseData = getDataFromResponse(response);
RequestTask.this.ns = NetworkState.SUCCESS;
}
catch (IOException e)
{
Log.i(GTG.TAG,"Error reading response from server",e);
RequestTask.this.ns = NetworkState.FAIL;
}
}
RequestTask.super.stNotify(RequestTask.this);
}
};
this.file = getCacheFile(url);
}
private byte[] getDataFromResponse(Response response) throws IOException {
BufferedSource source = response.body().source();
return source.readByteArray();
}
private File getCacheFile(String url) {
String fileName = cacheDir+"/"+Util.md5Hex(url);
return new File(fileName);
}
*/
/**
* The main loop of the task.
* <p>We take a look at the state, and decide what needs to be done accordingly,
* whether checking the cache or going out to the network
* </p>
*//*
@Override
protected void doWork() {
// Log.d(GTG.TAG,"GpsTrailerMapzenHttpHandler.doWork() "+this);
//note that doWork() will automatically be called again after returning
//until stExit() is called
if (cs == CacheState.NOT_TRIED) {
updateCacheState();
}
if(!respondedToCallback)
{
if (cs == CacheState.OK || cs == CacheState.FRESH)
{
respondToCallbackWithCache();
}
else if (ns == NetworkState.NOT_TRIED)
{
requestFromNetwork();
stWait(0,RequestTask.this);
return; //stwait does not pause the thread, but simply prevents
//doWork() from being called again until stNotify() is called.
}
else if (ns == NetworkState.FAIL)
{
//if we can't get a result from the network
//we will return the cache state, even if ancient
if(cs != CacheState.CORRUPT_OR_MISSING)
{
respondToCallbackWithCache();
}
else
{
//the cache is empty and the network failed so we give up
cb.onFailure(networkFailCall, networkFailException);
// Log.d(GTG.TAG,"Task finished unsuccesfully for "+RequestTask.this);
stExit();
return;
}
}
else // (ns == NetworkState.SUCCESS)
{
respondToCallbackWithResponse();
}
}
else if(ns == NetworkState.SUCCESS && !savedNetworkResponseToCache)
{
saveNetworkResponseToCache();
}
else { //nothing more to do
// Log.d(GTG.TAG,"Task finished for "+RequestTask.this);
stExit();
return;
}
//if we get here, doWork() will be called again
}
private void saveNetworkResponseToCache() {
try {
writeToFile(file, responseData);
}
catch (IOException e)
{
Log.e(GTG.TAG,"Can't save response to file "+file,e);
}
//even if we fail, we still mark that we saved the network response,
//because there is nothing that would be done differently had we
//succeeded.
savedNetworkResponseToCache = true;
}
private void writeToFile(File file, byte[] bytes) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytes);
fos.close();;
}
private void respondToCallbackWithResponse() {
try {
cb.onResponse(createResponseFromBytes(responseData));
respondedToCallback = true;
} catch (IOException e) {
Log.w(GTG.TAG,"Couldn't read network response for "+url+", assuming failed",e);
ns = NetworkState.FAIL;
}
}
private void requestFromNetwork() {
GpsTrailerMapzenHttpHandler.super.onRequest(url,myCb);
}
private Response createResponseFromBytes(byte [] data)
{
Response.Builder b = new Response.Builder();
b.body(ResponseBody.create(null,data));
b.code(200);
b.protocol(Protocol.HTTP_1_1);
b.request(okRequestBuilder.tag(url).url(url).build());
Response r = b.build();
return r;
}
private void respondToCallbackWithCache() {
//the tanzam code only looks at the body, so we just use dummy values for everything else
//Also, it reads the data in one go, and stores it all in memory, so there is no
//reason to do any fancy buffering with streams
//See MapController.startUrlRequest()
try {
cb.onResponse(createResponseFromBytes(getFileBytes(file)));
respondedToCallback = true;
} catch (IOException e) {
Log.w(GTG.TAG,"Couldn't read cache file "+file+", assuming corrupt",e);
cs = CacheState.CORRUPT_OR_MISSING;
}
}
private byte[] getFileBytes(File file) throws IOException {
long len = file.length();
if(len > MAX_TILE_LENGTH)
throw new IOException("tile too big, "+file+": "+len);
if(len < MIN_TILE_LENGTH)
throw new IOException("tile too small, "+file+": "+len);
byte [] out = new byte[(int)len];
FileInputStream fis = new FileInputStream(file);
com.rareventure.android.Util.readFully(fis, out);
fis.close();
return out;
}
private void updateCacheState() {
if(!file.exists()) {
cs = CacheState.CORRUPT_OR_MISSING;
return;
}
long ageMs = System.currentTimeMillis() - file.lastModified();
if(ageMs > GpsTrailerMapzenHttpHandler.prefs.ancientCacheMs)
cs = CacheState.ANCIENT;
else if(ageMs > GpsTrailerMapzenHttpHandler.prefs.okCacheMs)
cs = CacheState.OK;
else
cs = CacheState.FRESH;
}
}
*/
public static class Preferences
{
/**
* Any tile created before this period is considered ancient.
* This means that we will go to the network *first* and only
* if that fails, display this very old tile.
*/
public long ancientCacheMs = 1000l * 60 * 60 * 24 * 30; //one month
/**
* Any tile created before this period will be returned immediately
* for any request (as long as its not before ancientCacheMs).
* However, the network will still be probed afterwards to
* refresh this tile for the next time.
*/
public long okCacheMs = 1000l * 60 * 60 * 24 * 2; // two days
}
}