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