From 9c4e661e53f133841267c937274c7f74fb170d38 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sat, 25 Jun 2011 21:56:12 +0000 Subject: [PATCH] - 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 --- .../router/activity/I2PWebViewClient.java | 40 ++- .../android/router/activity/NewsActivity.java | 2 +- .../router/activity/PeersActivity.java | 2 +- .../android/router/activity/WebActivity.java | 2 +- .../android/router/service/RouterService.java | 28 +- .../i2p/android/router/service/StatusBar.java | 38 ++- src/net/i2p/android/router/util/AppCache.java | 246 ++++++++++++++++++ 7 files changed, 339 insertions(+), 19 deletions(-) create mode 100644 src/net/i2p/android/router/util/AppCache.java diff --git a/src/net/i2p/android/router/activity/I2PWebViewClient.java b/src/net/i2p/android/router/activity/I2PWebViewClient.java index 11c0d81a9..13d7dfa41 100644 --- a/src/net/i2p/android/router/activity/I2PWebViewClient.java +++ b/src/net/i2p/android/router/activity/I2PWebViewClient.java @@ -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 { + 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, url); + _view.loadDataWithBaseURL(url, d, t, e, history); } catch (Exception exc) { // CalledFromWrongThreadException cancel(false); diff --git a/src/net/i2p/android/router/activity/NewsActivity.java b/src/net/i2p/android/router/activity/NewsActivity.java index e48d9e90e..9a63ca91e 100644 --- a/src/net/i2p/android/router/activity/NewsActivity.java +++ b/src/net/i2p/android/router/activity/NewsActivity.java @@ -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); } diff --git a/src/net/i2p/android/router/activity/PeersActivity.java b/src/net/i2p/android/router/activity/PeersActivity.java index 2813b34c3..f775ee146 100644 --- a/src/net/i2p/android/router/activity/PeersActivity.java +++ b/src/net/i2p/android/router/activity/PeersActivity.java @@ -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); } diff --git a/src/net/i2p/android/router/activity/WebActivity.java b/src/net/i2p/android/router/activity/WebActivity.java index bab2d202a..8b5302f7d 100644 --- a/src/net/i2p/android/router/activity/WebActivity.java +++ b/src/net/i2p/android/router/activity/WebActivity.java @@ -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 diff --git a/src/net/i2p/android/router/service/RouterService.java b/src/net/i2p/android/router/service/RouterService.java index c6c06e0a0..98f61a891 100644 --- a/src/net/i2p/android/router/service/RouterService.java +++ b/src/net/i2p/android/router/service/RouterService.java @@ -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) { diff --git a/src/net/i2p/android/router/service/StatusBar.java b/src/net/i2p/android/router/service/StatusBar.java index 151f7b404..7f60cc530 100644 --- a/src/net/i2p/android/router/service/StatusBar.java +++ b/src/net/i2p/android/router/service/StatusBar.java @@ -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); + } + } } diff --git a/src/net/i2p/android/router/util/AppCache.java b/src/net/i2p/android/router/util/AppCache.java new file mode 100644 index 000000000..0b7ffdac0 --- /dev/null +++ b/src/net/i2p/android/router/util/AppCache.java @@ -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; + } + } +} -- GitLab