- 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.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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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