- Implement LRU file cache with max size and file count
- Use cache for going back in webview... Sadly you can't go forward after going back - Start brand new notification when starting or stopping router - Add uncaught exception handler to remove notification but not sure if it works
This commit is contained in:
@ -2,6 +2,7 @@ package net.i2p.android.router.activity;
|
|||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
@ -9,10 +10,13 @@ import android.webkit.WebView;
|
|||||||
import android.webkit.WebViewClient;
|
import android.webkit.WebViewClient;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
import net.i2p.android.apps.EepGetFetcher;
|
import net.i2p.android.apps.EepGetFetcher;
|
||||||
|
import net.i2p.android.router.util.AppCache;
|
||||||
import net.i2p.android.router.util.Util;
|
import net.i2p.android.router.util.Util;
|
||||||
import net.i2p.util.EepGet;
|
import net.i2p.util.EepGet;
|
||||||
|
|
||||||
@ -25,6 +29,10 @@ class I2PWebViewClient extends WebViewClient {
|
|||||||
private static final String FOOTER = "</body></html>";
|
private static final String FOOTER = "</body></html>";
|
||||||
private static final String ERROR_EEPSITE = HEADER + "Sorry, eepsites not yet supported" + FOOTER;
|
private static final String ERROR_EEPSITE = HEADER + "Sorry, eepsites not yet supported" + FOOTER;
|
||||||
|
|
||||||
|
public I2PWebViewClient(Context ctx) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||||
System.err.println("Should override? " + url);
|
System.err.println("Should override? " + url);
|
||||||
@ -94,6 +102,18 @@ class I2PWebViewClient extends WebViewClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadResource(WebView view, String url) {
|
||||||
|
Util.e("OLR URL: " + url);
|
||||||
|
super.onLoadResource(view, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||||
|
Util.e("ORE " + errorCode + " Desc: " + description + " URL: " + failingUrl);
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/******
|
/******
|
||||||
API 11 :(
|
API 11 :(
|
||||||
|
|
||||||
@ -183,7 +203,6 @@ class I2PWebViewClient extends WebViewClient {
|
|||||||
private static class BackgroundEepLoad extends BGLoad implements EepGet.StatusListener {
|
private static class BackgroundEepLoad extends BGLoad implements EepGet.StatusListener {
|
||||||
private final String _host;
|
private final String _host;
|
||||||
private int _total;
|
private int _total;
|
||||||
private String _data;
|
|
||||||
|
|
||||||
public BackgroundEepLoad(WebView view, String host) {
|
public BackgroundEepLoad(WebView view, String host) {
|
||||||
super(view);
|
super(view);
|
||||||
@ -214,8 +233,25 @@ class I2PWebViewClient extends WebViewClient {
|
|||||||
System.err.println("Fetch cancelled for " + url);
|
System.err.println("Fetch cancelled for " + url);
|
||||||
return Integer.valueOf(0);
|
return Integer.valueOf(0);
|
||||||
}
|
}
|
||||||
|
String history = url;
|
||||||
|
if (success) {
|
||||||
|
OutputStream out = null;
|
||||||
|
try {
|
||||||
|
out = AppCache.getInstance(_view.getContext()).createCacheFile(url);
|
||||||
|
out.write(d.getBytes(e));
|
||||||
|
history = AppCache.getInstance(_view.getContext()).addCacheFile(url);
|
||||||
|
Util.e("Stored cache in " + history);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
AppCache.getInstance(_view.getContext()).removeCacheFile(url);
|
||||||
|
Util.e("cache create error", ex);
|
||||||
|
} finally {
|
||||||
|
if (out != null) try { out.close(); } catch (IOException ioe) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
history = url;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
_view.loadDataWithBaseURL(url, d, t, e, url);
|
_view.loadDataWithBaseURL(url, d, t, e, history);
|
||||||
} catch (Exception exc) {
|
} catch (Exception exc) {
|
||||||
// CalledFromWrongThreadException
|
// CalledFromWrongThreadException
|
||||||
cancel(false);
|
cancel(false);
|
||||||
|
@ -40,7 +40,7 @@ public class NewsActivity extends I2PActivityBase {
|
|||||||
wv.getSettings().setLoadsImagesAutomatically(false);
|
wv.getSettings().setLoadsImagesAutomatically(false);
|
||||||
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
||||||
wv.getSettings().setUseWideViewPort(true);
|
wv.getSettings().setUseWideViewPort(true);
|
||||||
_wvClient = new I2PWebViewClient();
|
_wvClient = new I2PWebViewClient(this);
|
||||||
wv.setWebViewClient(_wvClient);
|
wv.setWebViewClient(_wvClient);
|
||||||
wv.getSettings().setBuiltInZoomControls(true);
|
wv.getSettings().setBuiltInZoomControls(true);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ public class PeersActivity extends I2PActivityBase {
|
|||||||
wv.getSettings().setLoadsImagesAutomatically(false);
|
wv.getSettings().setLoadsImagesAutomatically(false);
|
||||||
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
||||||
wv.getSettings().setUseWideViewPort(true);
|
wv.getSettings().setUseWideViewPort(true);
|
||||||
_wvClient = new I2PWebViewClient();
|
_wvClient = new I2PWebViewClient(this);
|
||||||
wv.setWebViewClient(_wvClient);
|
wv.setWebViewClient(_wvClient);
|
||||||
wv.getSettings().setBuiltInZoomControls(true);
|
wv.getSettings().setBuiltInZoomControls(true);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ public class WebActivity extends I2PActivityBase {
|
|||||||
TextView tv = (TextView) findViewById(R.id.browser_status);
|
TextView tv = (TextView) findViewById(R.id.browser_status);
|
||||||
tv.setText(WARNING);
|
tv.setText(WARNING);
|
||||||
WebView wv = (WebView) findViewById(R.id.browser_webview);
|
WebView wv = (WebView) findViewById(R.id.browser_webview);
|
||||||
_wvClient = new I2PWebViewClient();
|
_wvClient = new I2PWebViewClient(this);
|
||||||
wv.setWebViewClient(_wvClient);
|
wv.setWebViewClient(_wvClient);
|
||||||
wv.getSettings().setBuiltInZoomControls(true);
|
wv.getSettings().setBuiltInZoomControls(true);
|
||||||
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
|
||||||
|
@ -71,6 +71,8 @@ public class RouterService extends Service {
|
|||||||
init.initialize();
|
init.initialize();
|
||||||
//_apkPath = init.getAPKPath();
|
//_apkPath = init.getAPKPath();
|
||||||
_statusBar = new StatusBar(this);
|
_statusBar = new StatusBar(this);
|
||||||
|
// kill any old one... will this work?
|
||||||
|
_statusBar.off();
|
||||||
_binder = new RouterBinder(this);
|
_binder = new RouterBinder(this);
|
||||||
_handler = new Handler();
|
_handler = new Handler();
|
||||||
_updater = new Updater();
|
_updater = new Updater();
|
||||||
@ -99,14 +101,14 @@ public class RouterService extends Service {
|
|||||||
_receiver = new I2PReceiver(this);
|
_receiver = new I2PReceiver(this);
|
||||||
if (Util.isConnected(this)) {
|
if (Util.isConnected(this)) {
|
||||||
if (restart)
|
if (restart)
|
||||||
_statusBar.update("I2P is restarting");
|
_statusBar.replace("I2P is restarting");
|
||||||
else
|
else
|
||||||
_statusBar.update("I2P is starting up");
|
_statusBar.replace("I2P is starting up");
|
||||||
setState(State.STARTING);
|
setState(State.STARTING);
|
||||||
_starterThread = new Thread(new Starter());
|
_starterThread = new Thread(new Starter());
|
||||||
_starterThread.start();
|
_starterThread.start();
|
||||||
} else {
|
} else {
|
||||||
_statusBar.update("I2P is waiting for a network connection");
|
_statusBar.replace("I2P is waiting for a network connection");
|
||||||
setState(State.WAITING);
|
setState(State.WAITING);
|
||||||
_handler.postDelayed(new Waiter(), 10*1000);
|
_handler.postDelayed(new Waiter(), 10*1000);
|
||||||
}
|
}
|
||||||
@ -128,7 +130,7 @@ public class RouterService extends Service {
|
|||||||
synchronized (_stateLock) {
|
synchronized (_stateLock) {
|
||||||
if (_state != State.WAITING)
|
if (_state != State.WAITING)
|
||||||
return;
|
return;
|
||||||
_statusBar.update("Network connected, I2P is starting up");
|
_statusBar.replace("Network connected, I2P is starting up");
|
||||||
setState(State.STARTING);
|
setState(State.STARTING);
|
||||||
_starterThread = new Thread(new Starter());
|
_starterThread = new Thread(new Starter());
|
||||||
_starterThread.start();
|
_starterThread.start();
|
||||||
@ -260,7 +262,7 @@ public class RouterService extends Service {
|
|||||||
if (_state == State.STARTING)
|
if (_state == State.STARTING)
|
||||||
_starterThread.interrupt();
|
_starterThread.interrupt();
|
||||||
if (_state == State.STARTING || _state == State.RUNNING) {
|
if (_state == State.STARTING || _state == State.RUNNING) {
|
||||||
_statusBar.update("Stopping I2P");
|
_statusBar.replace("Stopping I2P");
|
||||||
Thread stopperThread = new Thread(new Stopper(State.MANUAL_STOPPING, State.MANUAL_STOPPED));
|
Thread stopperThread = new Thread(new Stopper(State.MANUAL_STOPPING, State.MANUAL_STOPPED));
|
||||||
stopperThread.start();
|
stopperThread.start();
|
||||||
}
|
}
|
||||||
@ -279,7 +281,7 @@ public class RouterService extends Service {
|
|||||||
if (_state == State.STARTING)
|
if (_state == State.STARTING)
|
||||||
_starterThread.interrupt();
|
_starterThread.interrupt();
|
||||||
if (_state == State.STARTING || _state == State.RUNNING) {
|
if (_state == State.STARTING || _state == State.RUNNING) {
|
||||||
_statusBar.update("Quitting I2P");
|
_statusBar.replace("Quitting I2P");
|
||||||
Thread stopperThread = new Thread(new Stopper(State.MANUAL_QUITTING, State.MANUAL_QUITTED));
|
Thread stopperThread = new Thread(new Stopper(State.MANUAL_QUITTING, State.MANUAL_QUITTED));
|
||||||
stopperThread.start();
|
stopperThread.start();
|
||||||
} else if (_state == State.WAITING) {
|
} else if (_state == State.WAITING) {
|
||||||
@ -299,7 +301,7 @@ public class RouterService extends Service {
|
|||||||
if (_state == State.STARTING)
|
if (_state == State.STARTING)
|
||||||
_starterThread.interrupt();
|
_starterThread.interrupt();
|
||||||
if (_state == State.STARTING || _state == State.RUNNING) {
|
if (_state == State.STARTING || _state == State.RUNNING) {
|
||||||
_statusBar.update("Network disconnected, stopping I2P");
|
_statusBar.replace("Network disconnected, stopping I2P");
|
||||||
// don't change state, let the shutdown hook do it
|
// don't change state, let the shutdown hook do it
|
||||||
Thread stopperThread = new Thread(new Stopper(State.NETWORK_STOPPING, State.NETWORK_STOPPING));
|
Thread stopperThread = new Thread(new Stopper(State.NETWORK_STOPPING, State.NETWORK_STOPPING));
|
||||||
stopperThread.start();
|
stopperThread.start();
|
||||||
@ -318,7 +320,7 @@ public class RouterService extends Service {
|
|||||||
synchronized (_stateLock) {
|
synchronized (_stateLock) {
|
||||||
if (!canManualStart())
|
if (!canManualStart())
|
||||||
return;
|
return;
|
||||||
_statusBar.update("I2P is starting up");
|
_statusBar.replace("I2P is starting up");
|
||||||
setState(State.STARTING);
|
setState(State.STARTING);
|
||||||
_starterThread = new Thread(new Starter());
|
_starterThread = new Thread(new Starter());
|
||||||
_starterThread.start();
|
_starterThread.start();
|
||||||
@ -338,7 +340,7 @@ public class RouterService extends Service {
|
|||||||
" Current state is: " + _state);
|
" Current state is: " + _state);
|
||||||
|
|
||||||
_handler.removeCallbacks(_updater);
|
_handler.removeCallbacks(_updater);
|
||||||
_statusBar.off(this);
|
_statusBar.off();
|
||||||
|
|
||||||
I2PReceiver rcvr = _receiver;
|
I2PReceiver rcvr = _receiver;
|
||||||
if (rcvr != null) {
|
if (rcvr != null) {
|
||||||
@ -356,7 +358,7 @@ public class RouterService extends Service {
|
|||||||
_starterThread.interrupt();
|
_starterThread.interrupt();
|
||||||
if (_state == State.STARTING || _state == State.RUNNING) {
|
if (_state == State.STARTING || _state == State.RUNNING) {
|
||||||
// should this be in a thread?
|
// should this be in a thread?
|
||||||
_statusBar.update("I2P is stopping");
|
_statusBar.replace("I2P is shutting down");
|
||||||
Thread stopperThread = new Thread(new Stopper(State.STOPPING, State.STOPPED));
|
Thread stopperThread = new Thread(new Stopper(State.STOPPING, State.STOPPED));
|
||||||
stopperThread.start();
|
stopperThread.start();
|
||||||
}
|
}
|
||||||
@ -386,7 +388,7 @@ public class RouterService extends Service {
|
|||||||
RouterContext ctx = _context;
|
RouterContext ctx = _context;
|
||||||
if (ctx != null)
|
if (ctx != null)
|
||||||
ctx.router().shutdown(Router.EXIT_HARD);
|
ctx.router().shutdown(Router.EXIT_HARD);
|
||||||
_statusBar.off(RouterService.this);
|
_statusBar.off();
|
||||||
System.err.println("********** Router shutdown complete");
|
System.err.println("********** Router shutdown complete");
|
||||||
synchronized (_stateLock) {
|
synchronized (_stateLock) {
|
||||||
if (_state == nextState)
|
if (_state == nextState)
|
||||||
@ -404,7 +406,7 @@ public class RouterService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
System.err.println(this + " shutdown hook" +
|
System.err.println(this + " shutdown hook" +
|
||||||
" Current state is: " + _state);
|
" Current state is: " + _state);
|
||||||
_statusBar.update("I2P is shutting down");
|
_statusBar.replace("I2P is shutting down");
|
||||||
I2PReceiver rcvr = _receiver;
|
I2PReceiver rcvr = _receiver;
|
||||||
if (rcvr != null) {
|
if (rcvr != null) {
|
||||||
synchronized(rcvr) {
|
synchronized(rcvr) {
|
||||||
@ -440,7 +442,7 @@ public class RouterService extends Service {
|
|||||||
public void run() {
|
public void run() {
|
||||||
System.err.println(this + " final shutdown hook" +
|
System.err.println(this + " final shutdown hook" +
|
||||||
" Current state is: " + _state);
|
" Current state is: " + _state);
|
||||||
_statusBar.off(RouterService.this);
|
_statusBar.off();
|
||||||
//I2PReceiver rcvr = _receiver;
|
//I2PReceiver rcvr = _receiver;
|
||||||
|
|
||||||
synchronized (_stateLock) {
|
synchronized (_stateLock) {
|
||||||
|
@ -6,6 +6,8 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import java.lang.Thread.UncaughtExceptionHandler;
|
||||||
|
|
||||||
import net.i2p.android.router.R;
|
import net.i2p.android.router.R;
|
||||||
import net.i2p.android.router.activity.MainActivity;
|
import net.i2p.android.router.activity.MainActivity;
|
||||||
|
|
||||||
@ -22,8 +24,10 @@ public class StatusBar {
|
|||||||
ctx = cx;
|
ctx = cx;
|
||||||
String ns = Context.NOTIFICATION_SERVICE;
|
String ns = Context.NOTIFICATION_SERVICE;
|
||||||
mgr = (NotificationManager)ctx.getSystemService(ns);
|
mgr = (NotificationManager)ctx.getSystemService(ns);
|
||||||
|
Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler(mgr));
|
||||||
|
|
||||||
int icon = R.drawable.ic_launcher_itoopie;
|
int icon = R.drawable.ic_launcher_itoopie;
|
||||||
|
// won't be shown if replace() is called
|
||||||
String text = "Starting I2P";
|
String text = "Starting I2P";
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
notif = new Notification(icon, text, now);
|
notif = new Notification(icon, text, now);
|
||||||
@ -32,6 +36,13 @@ public class StatusBar {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** remove and re-add */
|
||||||
|
public void replace(String tickerText) {
|
||||||
|
off();
|
||||||
|
notif.tickerText = tickerText;
|
||||||
|
update(tickerText);
|
||||||
|
}
|
||||||
|
|
||||||
public void update(String details) {
|
public void update(String details) {
|
||||||
String title = "I2P Status";
|
String title = "I2P Status";
|
||||||
update(title, details);
|
update(title, details);
|
||||||
@ -43,7 +54,32 @@ public class StatusBar {
|
|||||||
mgr.notify(ID, notif);
|
mgr.notify(ID, notif);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void off(Context ctx) {
|
public void off() {
|
||||||
mgr.cancel(ID);
|
mgr.cancel(ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* http://stackoverflow.com/questions/4028742/how-to-clear-a-notification-if-activity-crashes
|
||||||
|
*/
|
||||||
|
private static class CrashHandler implements Thread.UncaughtExceptionHandler {
|
||||||
|
|
||||||
|
private final Thread.UncaughtExceptionHandler defaultUEH;
|
||||||
|
private final NotificationManager mgr;
|
||||||
|
|
||||||
|
public CrashHandler(NotificationManager nMgr) {
|
||||||
|
defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||||
|
mgr = nMgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uncaughtException(Thread t, Throwable e) {
|
||||||
|
if (mgr != null) {
|
||||||
|
try {
|
||||||
|
mgr.cancel(ID);
|
||||||
|
} catch (Throwable ex) {}
|
||||||
|
}
|
||||||
|
System.err.println("In CrashHandler " + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
defaultUEH.uncaughtException(t, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
246
src/net/i2p/android/router/util/AppCache.java
Normal file
246
src/net/i2p/android/router/util/AppCache.java
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package net.i2p.android.router.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A least recently used cache with a max number of entries
|
||||||
|
* and a max total disk space.
|
||||||
|
*
|
||||||
|
* Like Android's CacheManager but usable.
|
||||||
|
*/
|
||||||
|
public class AppCache {
|
||||||
|
|
||||||
|
private static AppCache _instance;
|
||||||
|
private static File _cacheDir;
|
||||||
|
private static long _totalSize;
|
||||||
|
/** the LRU cache */
|
||||||
|
private final Map<Integer, Object> _cache;
|
||||||
|
|
||||||
|
private static final Integer DUMMY = Integer.valueOf(0);
|
||||||
|
private static final String DIR_NAME = "appCache";
|
||||||
|
/** fragment into this many subdirectories */
|
||||||
|
private static final int NUM_DIRS = 32;
|
||||||
|
private static final int MAX_FILES = 1024;
|
||||||
|
/** total used space */
|
||||||
|
private static final long MAX_SPACE = 1024 * 1024;
|
||||||
|
|
||||||
|
|
||||||
|
public static AppCache getInstance(Context ctx) {
|
||||||
|
synchronized (AppCache.class) {
|
||||||
|
if (_instance == null)
|
||||||
|
_instance = new AppCache(ctx);
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppCache(Context ctx) {
|
||||||
|
_cacheDir = new File(ctx.getCacheDir(), DIR_NAME);
|
||||||
|
_cacheDir.mkdir();
|
||||||
|
Util.e("AppCache cache dir " + _cacheDir);
|
||||||
|
_cache = new LHM(MAX_FILES);
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller MUST close stream AND call either
|
||||||
|
* addCacheFile() or removeCacheFile() after the data is written.
|
||||||
|
*/
|
||||||
|
public OutputStream createCacheFile(String key) throws IOException {
|
||||||
|
// remove any old file so the total stays correct
|
||||||
|
removeCacheFile(key);
|
||||||
|
File f = toFile(key);
|
||||||
|
f.getParentFile().mkdirs();
|
||||||
|
return new FileOutputStream(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a previously written file to the cache.
|
||||||
|
* Return a file:/// uri for the cached content in question.
|
||||||
|
*/
|
||||||
|
public String addCacheFile(String key) {
|
||||||
|
int hash = toHash(key);
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache.put(Integer.valueOf(hash), DUMMY);
|
||||||
|
}
|
||||||
|
return Uri.fromFile(toFile(hash)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a previously written file from the cache.
|
||||||
|
*/
|
||||||
|
public void removeCacheFile(String key) {
|
||||||
|
int hash = toHash(key);
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache.remove(Integer.valueOf(hash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a file:/// uri for any cached content in question.
|
||||||
|
* The file may or may not exist, and it may be deleted at any time.
|
||||||
|
*/
|
||||||
|
public String getCacheFile(String key) {
|
||||||
|
int hash = toHash(key);
|
||||||
|
// poke the LRU
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache.get(Integer.valueOf(hash));
|
||||||
|
}
|
||||||
|
return Uri.fromFile(toFile(hash)).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
////// private below here
|
||||||
|
|
||||||
|
private void initialize() {
|
||||||
|
_totalSize = 0;
|
||||||
|
List<File> fileList = new ArrayList(MAX_FILES);
|
||||||
|
long total = enumerate(_cacheDir, fileList);
|
||||||
|
Util.e("AppCache found " + fileList.size() + " files totalling " + total + " bytes");
|
||||||
|
Collections.sort(fileList, new FileComparator());
|
||||||
|
// oldest first, delete if too big else add to LHM
|
||||||
|
for (File f : fileList) {
|
||||||
|
if (total > MAX_SPACE) {
|
||||||
|
total -= f.length();
|
||||||
|
f.delete();
|
||||||
|
} else {
|
||||||
|
addToCache(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Util.e("after init " + _cache.size() + " files totalling " + total + " bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** oldest first */
|
||||||
|
private static class FileComparator implements Comparator<File> {
|
||||||
|
public int compare(File l, File r) {
|
||||||
|
return (int) (l.lastModified() - r.lastModified());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** get all the files, deleting empty ones on the way, returning total size */
|
||||||
|
private static long enumerate(File dir, List<File> fileList) {
|
||||||
|
long rv = 0;
|
||||||
|
File[] files = dir.listFiles();
|
||||||
|
if (files == null)
|
||||||
|
return 0;
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
File f = files[i];
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
rv += enumerate(f, fileList);
|
||||||
|
} else {
|
||||||
|
long len = f.length();
|
||||||
|
if (len > 0) {
|
||||||
|
fileList.add(f);
|
||||||
|
rv += len;
|
||||||
|
} else {
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** for initialization only */
|
||||||
|
private void addToCache(File f) {
|
||||||
|
try {
|
||||||
|
int hash = toHash(f);
|
||||||
|
synchronized(_cache) {
|
||||||
|
_cache.put(Integer.valueOf(hash), DUMMY);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException iae) {
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** for initialization only */
|
||||||
|
private static int toHash(File f) throws IllegalArgumentException {
|
||||||
|
String path = f.getAbsolutePath();
|
||||||
|
int slash = path.lastIndexOf("/");
|
||||||
|
String basename = path.substring(slash);
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(basename);
|
||||||
|
} catch (NumberFormatException nfe) {
|
||||||
|
throw new IllegalArgumentException("bad file name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** just use the hashcode for the hash */
|
||||||
|
private static int toHash(String key) {
|
||||||
|
return key.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /path/to/cache/dir/(hashCode(key) % 32)/hashCode(key)
|
||||||
|
*/
|
||||||
|
private static File toFile(String key) {
|
||||||
|
int hash = toHash(key);
|
||||||
|
return toFile(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File toFile(int hash) {
|
||||||
|
int dir = hash % NUM_DIRS;
|
||||||
|
return new File(_cacheDir, dir + "/" + hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An LRU set of hashcodes, implemented on a HashMap.
|
||||||
|
* We use a dummy for the value to save space, because the
|
||||||
|
* hashcode key is reversable to the file name.
|
||||||
|
* The put and remove methods are overridden to
|
||||||
|
* keep the total size counter updated, and to delete the underlying file
|
||||||
|
* on remove.
|
||||||
|
*/
|
||||||
|
private static class LHM extends LinkedHashMap<Integer, Object> {
|
||||||
|
private final int _max;
|
||||||
|
|
||||||
|
public LHM(int max) {
|
||||||
|
super(max, 0.75f, true);
|
||||||
|
_max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add the entry, and update the total size */
|
||||||
|
@Override
|
||||||
|
public Object put(Integer key, Object value) {
|
||||||
|
Object rv = super.put(key, value);
|
||||||
|
File f = toFile(key.intValue());
|
||||||
|
if (f.exists()) {
|
||||||
|
_totalSize += f.length();
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove the entry and the file, and update the total size */
|
||||||
|
@Override
|
||||||
|
public Object remove(Object key) {
|
||||||
|
Object rv = super.remove(key);
|
||||||
|
if (rv != null && key instanceof Integer) {
|
||||||
|
File f = toFile(((Integer)key).intValue());
|
||||||
|
if (f.exists()) {
|
||||||
|
_totalSize -= f.length();
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<Integer, Object> eldest) {
|
||||||
|
if (size() > _max || _totalSize > MAX_SPACE) {
|
||||||
|
Integer key = eldest.getKey();
|
||||||
|
remove(key);
|
||||||
|
}
|
||||||
|
// we modified the map, we must return false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user