- 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:
zzz
2011-06-25 21:56:12 +00:00
parent a4215cf76e
commit 9c4e661e53
7 changed files with 339 additions and 19 deletions

View File

@ -2,6 +2,7 @@ package net.i2p.android.router.activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.view.Gravity;
@ -9,10 +10,13 @@ import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import net.i2p.android.apps.EepGetFetcher;
import net.i2p.android.router.util.AppCache;
import net.i2p.android.router.util.Util;
import net.i2p.util.EepGet;
@ -25,6 +29,10 @@ class I2PWebViewClient extends WebViewClient {
private static final String FOOTER = "</body></html>";
private static final String ERROR_EEPSITE = HEADER + "Sorry, eepsites not yet supported" + FOOTER;
public I2PWebViewClient(Context ctx) {
super();
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String 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 :(
@ -183,7 +203,6 @@ class I2PWebViewClient extends WebViewClient {
private static class BackgroundEepLoad extends BGLoad implements EepGet.StatusListener {
private final String _host;
private int _total;
private String _data;
public BackgroundEepLoad(WebView view, String host) {
super(view);
@ -214,8 +233,25 @@ class I2PWebViewClient extends WebViewClient {
System.err.println("Fetch cancelled for " + url);
return Integer.valueOf(0);
}
String history = url;
if (success) {
OutputStream out = null;
try {
_view.loadDataWithBaseURL(url, d, t, e, url);
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 {
_view.loadDataWithBaseURL(url, d, t, e, history);
} catch (Exception exc) {
// CalledFromWrongThreadException
cancel(false);

View File

@ -40,7 +40,7 @@ public class NewsActivity extends I2PActivityBase {
wv.getSettings().setLoadsImagesAutomatically(false);
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
wv.getSettings().setUseWideViewPort(true);
_wvClient = new I2PWebViewClient();
_wvClient = new I2PWebViewClient(this);
wv.setWebViewClient(_wvClient);
wv.getSettings().setBuiltInZoomControls(true);
}

View File

@ -31,7 +31,7 @@ public class PeersActivity extends I2PActivityBase {
wv.getSettings().setLoadsImagesAutomatically(false);
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
wv.getSettings().setUseWideViewPort(true);
_wvClient = new I2PWebViewClient();
_wvClient = new I2PWebViewClient(this);
wv.setWebViewClient(_wvClient);
wv.getSettings().setBuiltInZoomControls(true);
}

View File

@ -33,7 +33,7 @@ public class WebActivity extends I2PActivityBase {
TextView tv = (TextView) findViewById(R.id.browser_status);
tv.setText(WARNING);
WebView wv = (WebView) findViewById(R.id.browser_webview);
_wvClient = new I2PWebViewClient();
_wvClient = new I2PWebViewClient(this);
wv.setWebViewClient(_wvClient);
wv.getSettings().setBuiltInZoomControls(true);
// http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855

View File

@ -71,6 +71,8 @@ public class RouterService extends Service {
init.initialize();
//_apkPath = init.getAPKPath();
_statusBar = new StatusBar(this);
// kill any old one... will this work?
_statusBar.off();
_binder = new RouterBinder(this);
_handler = new Handler();
_updater = new Updater();
@ -99,14 +101,14 @@ public class RouterService extends Service {
_receiver = new I2PReceiver(this);
if (Util.isConnected(this)) {
if (restart)
_statusBar.update("I2P is restarting");
_statusBar.replace("I2P is restarting");
else
_statusBar.update("I2P is starting up");
_statusBar.replace("I2P is starting up");
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
} else {
_statusBar.update("I2P is waiting for a network connection");
_statusBar.replace("I2P is waiting for a network connection");
setState(State.WAITING);
_handler.postDelayed(new Waiter(), 10*1000);
}
@ -128,7 +130,7 @@ public class RouterService extends Service {
synchronized (_stateLock) {
if (_state != State.WAITING)
return;
_statusBar.update("Network connected, I2P is starting up");
_statusBar.replace("Network connected, I2P is starting up");
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
@ -260,7 +262,7 @@ public class RouterService extends Service {
if (_state == State.STARTING)
_starterThread.interrupt();
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));
stopperThread.start();
}
@ -279,7 +281,7 @@ public class RouterService extends Service {
if (_state == State.STARTING)
_starterThread.interrupt();
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));
stopperThread.start();
} else if (_state == State.WAITING) {
@ -299,7 +301,7 @@ public class RouterService extends Service {
if (_state == State.STARTING)
_starterThread.interrupt();
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
Thread stopperThread = new Thread(new Stopper(State.NETWORK_STOPPING, State.NETWORK_STOPPING));
stopperThread.start();
@ -318,7 +320,7 @@ public class RouterService extends Service {
synchronized (_stateLock) {
if (!canManualStart())
return;
_statusBar.update("I2P is starting up");
_statusBar.replace("I2P is starting up");
setState(State.STARTING);
_starterThread = new Thread(new Starter());
_starterThread.start();
@ -338,7 +340,7 @@ public class RouterService extends Service {
" Current state is: " + _state);
_handler.removeCallbacks(_updater);
_statusBar.off(this);
_statusBar.off();
I2PReceiver rcvr = _receiver;
if (rcvr != null) {
@ -356,7 +358,7 @@ public class RouterService extends Service {
_starterThread.interrupt();
if (_state == State.STARTING || _state == State.RUNNING) {
// 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));
stopperThread.start();
}
@ -386,7 +388,7 @@ public class RouterService extends Service {
RouterContext ctx = _context;
if (ctx != null)
ctx.router().shutdown(Router.EXIT_HARD);
_statusBar.off(RouterService.this);
_statusBar.off();
System.err.println("********** Router shutdown complete");
synchronized (_stateLock) {
if (_state == nextState)
@ -404,7 +406,7 @@ public class RouterService extends Service {
public void run() {
System.err.println(this + " shutdown hook" +
" Current state is: " + _state);
_statusBar.update("I2P is shutting down");
_statusBar.replace("I2P is shutting down");
I2PReceiver rcvr = _receiver;
if (rcvr != null) {
synchronized(rcvr) {
@ -440,7 +442,7 @@ public class RouterService extends Service {
public void run() {
System.err.println(this + " final shutdown hook" +
" Current state is: " + _state);
_statusBar.off(RouterService.this);
_statusBar.off();
//I2PReceiver rcvr = _receiver;
synchronized (_stateLock) {

View File

@ -6,6 +6,8 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import java.lang.Thread.UncaughtExceptionHandler;
import net.i2p.android.router.R;
import net.i2p.android.router.activity.MainActivity;
@ -22,8 +24,10 @@ public class StatusBar {
ctx = cx;
String ns = Context.NOTIFICATION_SERVICE;
mgr = (NotificationManager)ctx.getSystemService(ns);
Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler(mgr));
int icon = R.drawable.ic_launcher_itoopie;
// won't be shown if replace() is called
String text = "Starting I2P";
long now = System.currentTimeMillis();
notif = new Notification(icon, text, now);
@ -32,6 +36,13 @@ public class StatusBar {
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) {
String title = "I2P Status";
update(title, details);
@ -43,7 +54,32 @@ public class StatusBar {
mgr.notify(ID, notif);
}
public void off(Context ctx) {
public void off() {
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);
}
}
}

View 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;
}
}
}