diff --git a/res/drawable/ic_menu_refresh.png b/res/drawable/ic_menu_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..77d70dd4f0534271b71ef4eb87f5a7a917d944fa Binary files /dev/null and b/res/drawable/ic_menu_refresh.png differ diff --git a/res/menu/menu1.xml b/res/menu/menu1.xml index 565b164970193e812d5fea26495818f30c7c537e..6ca00c57998ce960eae1488f3e61ff7364e578f6 100755 --- a/res/menu/menu1.xml +++ b/res/menu/menu1.xml @@ -11,6 +11,10 @@ android:id="@+id/menu_addressbook" android:icon="@drawable/ic_menu_friendslist" > </item> + <item android:title="Reload" + android:id="@+id/menu_reload" + android:icon="@drawable/ic_menu_refresh" > + </item> <item android:title="I2P Home" android:id="@+id/menu_home" android:icon="@drawable/ic_menu_home" > diff --git a/res/raw/releasenotes_txt b/res/raw/releasenotes_txt index ece82803225e532c1490fd0df0ebe4be3c000c34..cc552e84a608681b5762910d48e58f8ae82324b1 100644 --- a/res/raw/releasenotes_txt +++ b/res/raw/releasenotes_txt @@ -1,6 +1,6 @@ ******* Please read all of the following ******* -WARNING - This is ALPHA SOFTWARE. It may crash your phone. Do not rely upon it for strong anonymity. Tunnels may be as short as one hop. +WARNING - This is ALPHA SOFTWARE. It may crash your phone. Do not rely upon it for strong anonymity. Tunnels may be as short as one hop. There may be serious security holes in the app. Minimum Android OS is 2.2 (API 8). The app is only tested on the Motorola Droid. It uses a lot of RAM. You need at least 256 MB of RAM. 512 should be much better. diff --git a/src/net/i2p/android/router/activity/I2PActivityBase.java b/src/net/i2p/android/router/activity/I2PActivityBase.java index d4ba6163d20f7c0e7f18adc6c211560370900e2a..69ecfa4eaef75dadaaeda393a6862c533f823269 100644 --- a/src/net/i2p/android/router/activity/I2PActivityBase.java +++ b/src/net/i2p/android/router/activity/I2PActivityBase.java @@ -149,6 +149,9 @@ public abstract class I2PActivityBase extends Activity { MenuItem addressbook = menu.findItem(R.id.menu_addressbook); addressbook.setVisible(showAddressbook); addressbook.setEnabled(showAddressbook); + MenuItem reload = menu.findItem(R.id.menu_reload); + reload.setVisible(showAddressbook); + reload.setEnabled(showAddressbook); return super.onPrepareOptionsMenu(menu); } @@ -171,6 +174,7 @@ public abstract class I2PActivityBase extends Activity { startActivity(i3); return true; + case R.id.menu_reload: case R.id.menu_start: case R.id.menu_stop: default: diff --git a/src/net/i2p/android/router/activity/I2PWebViewClient.java b/src/net/i2p/android/router/activity/I2PWebViewClient.java index 63742a47cb5c557b80a9217f3b967d89a32a84cb..f7981cf945f1bfbd854ba678f79fe46655389442 100644 --- a/src/net/i2p/android/router/activity/I2PWebViewClient.java +++ b/src/net/i2p/android/router/activity/I2PWebViewClient.java @@ -7,16 +7,16 @@ import android.content.DialogInterface; import android.net.Uri; import android.os.AsyncTask; import android.view.Gravity; +import android.view.View; 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.provider.CacheProvider; import net.i2p.android.router.util.AppCache; import net.i2p.android.router.util.Util; import net.i2p.util.EepGet; @@ -38,39 +38,34 @@ class I2PWebViewClient extends WebViewClient { public boolean shouldOverrideUrlLoading(WebView view, String url) { Util.e("Should override? " + url); view.stopLoading(); - try { - URI uri = new URI(url); + + Uri uri = Uri.parse(url); String s = uri.getScheme(); if (s == null) { - Toast toast = Toast.makeText(view.getContext(), "Bad URL " + url, Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); + fail(view, "Bad URL " + url); return true; } s = s.toLowerCase(); - if (!(s.equals("http") || s.equals("https"))) + if (!(s.equals("http") || s.equals("https") || + s.equals("content"))) { + Util.e("Not loading URL " + url); return false; + } String h = uri.getHost(); if (h == null) { - Toast toast = Toast.makeText(view.getContext(), "Bad URL " + url, Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); + fail(view, "Bad URL " + url); return true; } if (!Util.isConnected(view.getContext())) { - Toast toast = Toast.makeText(view.getContext(), "No Internet connection is available", Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); + fail(view, "No Internet connection is available"); return true; } h = h.toLowerCase(); if (h.endsWith(".i2p")) { if (!s.equals("http")) { - Toast toast = Toast.makeText(view.getContext(), "Bad URL " + url, Toast.LENGTH_LONG); - toast.setGravity(Gravity.CENTER, 0, 0); - toast.show(); + fail(view, "Bad URL " + url); return true; } @@ -89,18 +84,33 @@ class I2PWebViewClient extends WebViewClient { _lastTask = task; task.execute(url); } else { + if (s.equals("content")) { + // canonicalize to append query to path + // because the resolver doesn't send a query to the provider + Uri canon = CacheProvider.getContentUri(uri); + if (canon == null) { + fail(view, "Bad URL " + url); + return true; + } + url = canon.toString(); + } view.getSettings().setLoadsImagesAutomatically(true); ///////// API 8 view.getSettings().setBlockNetworkLoads(false); //view.loadUrl(url); BGLoad task = new BackgroundLoad(view); _lastTask = task; + Util.e("Fetching via web or resource: " + url); task.execute(url); } return true; - } catch (URISyntaxException use) { - return false; - } + } + + private static void fail(View v, String s) { + Util.e("Fail toast: " + s); + Toast toast = Toast.makeText(v.getContext(), s, Toast.LENGTH_LONG); + toast.setGravity(Gravity.CENTER, 0, 0); + toast.show(); } @Override @@ -212,6 +222,21 @@ class I2PWebViewClient extends WebViewClient { protected Integer doInBackground(String... urls) { String url = urls[0]; + Uri uri = Uri.parse(url); + if (AppCache.getInstance(_view.getContext()).getCacheFile(uri).exists()) { + Uri resUri = AppCache.getInstance(_view.getContext()).getCacheUri(uri); + Util.e("Loading " + url + " from resource cache " + resUri); + _view.getSettings().setLoadsImagesAutomatically(true); + _view.getSettings().setBlockNetworkLoads(false); + try { + _view.loadUrl(resUri.toString()); + } catch (Exception e) { + // CalledFromWrongThreadException + cancel(false); + } + return Integer.valueOf(0); + } + publishProgress(Integer.valueOf(-1)); EepGetFetcher fetcher = new EepGetFetcher(url); fetcher.addStatusListener(this); @@ -236,8 +261,6 @@ class I2PWebViewClient extends WebViewClient { } String history = url; if (success) { - // TODO switch back to Uri - Uri uri = Uri.parse(url); OutputStream out = null; try { out = AppCache.getInstance(_view.getContext()).createCacheFile(uri); diff --git a/src/net/i2p/android/router/activity/MainActivity.java b/src/net/i2p/android/router/activity/MainActivity.java index ddc62400236bd6472edc6e603795cc2e8512dc54..102afd11fd66e4c950ceeb0728b7a94e5094c8a3 100644 --- a/src/net/i2p/android/router/activity/MainActivity.java +++ b/src/net/i2p/android/router/activity/MainActivity.java @@ -89,7 +89,7 @@ public class MainActivity extends I2PActivityBase { b.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { Intent intent = new Intent(view.getContext(), WebActivity.class); - // default is to display the welcome_html resource + intent.putExtra(WebActivity.HTML_RESOURCE_ID, R.raw.welcome_html); startActivity(intent); } }); diff --git a/src/net/i2p/android/router/activity/WebActivity.java b/src/net/i2p/android/router/activity/WebActivity.java index 8b5302f7d26bbda7aebdd600931ae137bdc1f138..98a2fa5113155196515d03ea2770c18d3a3bb7fb 100644 --- a/src/net/i2p/android/router/activity/WebActivity.java +++ b/src/net/i2p/android/router/activity/WebActivity.java @@ -23,7 +23,7 @@ public class WebActivity extends I2PActivityBase { final static String HTML_RESOURCE_ID = "html_resource_id"; private static final String WARNING = "Warning - " + "any non-I2P links visited in this window are fetched over the regular internet and are " + - "not anonymous. I2P pages do not load images or CSS.\n"; + "not anonymous. I2P pages may not load images or CSS."; @Override public void onCreate(Bundle savedInstanceState) @@ -47,8 +47,10 @@ public class WebActivity extends I2PActivityBase { _wvClient.shouldOverrideUrlLoading(wv, uri.toString()); } else { wv.getSettings().setLoadsImagesAutomatically(false); - int id = intent.getIntExtra(HTML_RESOURCE_ID, R.raw.welcome_html); - loadResource(wv, id); + int id = intent.getIntExtra(HTML_RESOURCE_ID, 0); + // no default, so restart should keep previous view + if (id != 0) + loadResource(wv, id); } } diff --git a/src/net/i2p/android/router/provider/CacheProvider.java b/src/net/i2p/android/router/provider/CacheProvider.java index 26276b7915a5c2e47177540e3563eadead750530..dc34887daa49257cf2c89e464164c47904202c22 100644 --- a/src/net/i2p/android/router/provider/CacheProvider.java +++ b/src/net/i2p/android/router/provider/CacheProvider.java @@ -2,6 +2,7 @@ package net.i2p.android.router.provider; import android.content.ContentProvider; import android.content.ContentValues; +import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -10,8 +11,9 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import net.i2p.android.apps.EepGetFetcher; import net.i2p.android.router.util.AppCache; @@ -39,11 +41,15 @@ public class CacheProvider extends ContentProvider { // FIXME not persistent, use SharedPrefs /** content:// Uri to absolute path of the file */ - private Map<Uri, String> _uriMap; + private SharedPreferences _sharedPrefs; - private static final String NONCE = Integer.toString(Math.abs((new java.util.Random()).nextInt())); + private static final String SHARED_PREFS = "net.i2p.android.router.provider.CacheProvider"; + //private static final String NONCE = Integer.toString(Math.abs((new java.util.Random()).nextInt())); + private static final String NONCE = "0"; + private static final String SCHEME = "content"; + private static final String AUTHORITY = "net.i2p.android.router"; /** includes the nonce */ - public static final Uri CONTENT_URI = Uri.parse("content://net.i2p.android.router/" + NONCE); + public static final Uri CONTENT_URI = Uri.parse(SCHEME + "://" + AUTHORITY + '/' + NONCE); /** the database key */ public static final String DATA = "_data"; private static final String QUERY_MARKER = "!!QUERY!!"; @@ -54,7 +60,10 @@ public class CacheProvider extends ContentProvider { private static final String ERROR_FOOTER = "</body></html>"; /** - * Generate a cache content URI for a given URI key + * Generate a cache content (resource) URI for a given URI key + * If the key is already a resource URI, canonicalize it + * by twizzling the query if necessary + * * @param key must contain a scheme, authority and path * @return null on error */ @@ -65,6 +74,27 @@ public class CacheProvider extends ContentProvider { if (s == null || a == null || p == null) return null; String q = key.getEncodedQuery(); + + // canonicalize resource URI + if (s.equals(SCHEME)) { + if (q == null || !a.equals(AUTHORITY)) + return key; + if (p.contains(QUERY_MARKER)) { + Util.e("Key contains both queries ?!? " + key); + return null; + } + // twizzle query + StringBuilder buf = new StringBuilder(128); + buf.append(s).append("://") + .append(a); + if (!p.startsWith("/")) + buf.append('/'); + buf.append(p); + buf.append(QUERY_MARKER).append(q); + return Uri.parse(buf.toString()); + } + + // convert http URI to resource StringBuilder buf = new StringBuilder(128); buf.append(CONTENT_URI).append('/') .append(s).append('/') @@ -81,7 +111,7 @@ public class CacheProvider extends ContentProvider { public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { Util.e("CacheProvider open " + uri); // map the resource URI to a local file URI and return it if it exists - String filePath = _uriMap.get(uri); + String filePath = get(uri); if (filePath != null) { try { File file = new File(filePath); @@ -90,10 +120,24 @@ public class CacheProvider extends ContentProvider { return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); } catch (FileNotFoundException fnfe) { Util.e("CacheProvider not found", fnfe); - _uriMap.remove(uri); + remove(uri); } } Util.e("CacheProvider not in cache " + uri); + + Uri newUri = getI2PUri(uri); + Util.e("CacheProvider fetching: " + newUri); + return eepFetch(newUri); + } + + /** + * Generate an i2p URI for a resource URI + * + * @param uri must contain a scheme, authority and path with nonce etc. as defined above + * @return non-null + * @throws FNFE on error + */ + private static Uri getI2PUri(Uri uri) throws FileNotFoundException { String resPath = uri.getEncodedPath(); if (resPath == null) throw new FileNotFoundException("Bad uri no path? " + uri); @@ -107,8 +151,8 @@ public class CacheProvider extends ContentProvider { if (query == null) { int marker = realPath.indexOf(QUERY_MARKER); if (marker >= 0) { - realPath = realPath.substring(0, marker); query = realPath.substring(marker + QUERY_MARKER.length()); + realPath = realPath.substring(0, marker); } } String debug = "CacheProvider nonce: " + nonce + " scheme: " + scheme + " host: " + host + " realPath: " + realPath + " query: " + query; @@ -118,21 +162,14 @@ public class CacheProvider extends ContentProvider { (host == null) || (!host.endsWith(".i2p"))) throw new FileNotFoundException(debug); - String newUri = scheme + "://" + host + '/' + realPath; + String i2pUri = scheme + "://" + host + '/' + realPath; if (query != null) - newUri += '?' + query; - - Util.e("CacheProvider fetching: " + newUri); - return eepFetch(newUri); + i2pUri += '?' + query; + return Uri.parse(i2pUri); } - private ParcelFileDescriptor eepFetch(String url) throws FileNotFoundException { - AppCache cache = AppCache.getInstance(); - if (cache == null) { - Util.e("app cache uninitialized " + url); - throw new FileNotFoundException("uninitialized"); - } - Uri uri = Uri.parse(url); + private ParcelFileDescriptor eepFetch(Uri uri) throws FileNotFoundException { + AppCache cache = AppCache.getInstance(getContext()); OutputStream out; try { out = cache.createCacheFile(uri); @@ -140,7 +177,7 @@ public class CacheProvider extends ContentProvider { throw new FileNotFoundException(ioe.toString()); } // in this constructor we don't use the error output, for now - EepGetFetcher fetcher = new EepGetFetcher(url, out); + EepGetFetcher fetcher = new EepGetFetcher(uri.toString(), out); boolean success = fetcher.fetch(); if (success) { File file = cache.getCacheFile(uri); @@ -161,8 +198,8 @@ public class CacheProvider extends ContentProvider { public int delete(Uri uri, String selection, String[] selectionArgs) { Util.e("CacheProvider delete " + uri); - String deleted = _uriMap.remove(uri); - return deleted != null ? 1 : 0; + boolean deleted = remove(uri); + return deleted ? 1 : 0; } public String getType(Uri uri) { @@ -177,12 +214,13 @@ public class CacheProvider extends ContentProvider { Util.e("CacheProvider insert " + uri); String fileURI = values.getAsString(DATA); if (fileURI != null) - _uriMap.put(uri, fileURI); + put(uri, fileURI); return uri; } public boolean onCreate() { - _uriMap = new ConcurrentHashMap(); + _sharedPrefs = getContext().getSharedPreferences(SHARED_PREFS, 0); + cleanup(); return true; } @@ -195,4 +233,59 @@ public class CacheProvider extends ContentProvider { public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } + + ///// Map stuff + + private void cleanup() { + List<String> toDelete = new ArrayList(); + Map<String, ?> map = _sharedPrefs.getAll(); + for (Map.Entry<String, ?> e : map.entrySet()) { + String path = (String) e.getValue(); + File f = new File(path); + if (!f.exists()) + toDelete.add(e.getKey()); + } + if (!toDelete.isEmpty()) { + SharedPreferences.Editor edit = _sharedPrefs.edit(); + for (String key : toDelete) { + edit.remove(key); + } + edit.commit(); + } + } + + private String get(Uri uri) { + return getPref(uri.toString()); + } + + private void put(Uri uri, String fileURI) { + setPref(uri.toString(), fileURI); + } + + /** @return true if it was removed */ + private boolean remove(Uri uri) { + String old = getPref(uri.toString()); + boolean success = deletePref(uri.toString()); + return success && old != null; + } + + /** @return null if not found */ + private String getPref(String pref) { + return _sharedPrefs.getString(pref, null); + } + + /** @return success */ + private boolean setPref(String pref, String val) { + SharedPreferences.Editor edit = _sharedPrefs.edit(); + edit.putString(pref, val); + return edit.commit(); + } + + /** @return success */ + private boolean deletePref(String pref) { + SharedPreferences.Editor edit = _sharedPrefs.edit(); + edit.remove(pref); + return edit.commit(); + } + } diff --git a/src/net/i2p/android/router/service/RouterService.java b/src/net/i2p/android/router/service/RouterService.java index 98f61a891183a2c2ca22174576f0e00aa557766b..3f008bc6fcc30131b958d56d38cf2088e241e09d 100644 --- a/src/net/i2p/android/router/service/RouterService.java +++ b/src/net/i2p/android/router/service/RouterService.java @@ -182,6 +182,8 @@ public class RouterService extends Service { } } + private boolean _hadTunnels; + private void updateStatus(RouterContext ctx) { int active = ctx.commSystem().countActivePeers(); int known = Math.max(ctx.netDb().getKnownRouters() - 1, 0); @@ -210,6 +212,14 @@ public class RouterService extends Service { "; Expl " + inEx + '/' + outEx + "; Client " + inCl + '/' + outCl; + boolean haveTunnels = inCl > 0 && outCl > 0; + if (haveTunnels != _hadTunnels) { + if (haveTunnels) + _statusBar.replace("Client tunnels are ready"); + else + _statusBar.replace("Client tunnels are down"); + _hadTunnels = haveTunnels; + } _statusBar.update(status, details); } @@ -448,6 +458,7 @@ public class RouterService extends Service { synchronized (_stateLock) { // null out to release the memory _context = null; + Runtime.getRuntime().gc(); if (_state == State.STARTING) _starterThread.interrupt(); if (_state == State.MANUAL_STOPPING) { diff --git a/src/net/i2p/android/router/util/AppCache.java b/src/net/i2p/android/router/util/AppCache.java index bdaacf8e75fdaaaaa1525d009188fb5ce8e21a3d..c130613a840b658fc4de869a13ac623fff61f154 100644 --- a/src/net/i2p/android/router/util/AppCache.java +++ b/src/net/i2p/android/router/util/AppCache.java @@ -51,6 +51,9 @@ public class AppCache { return _instance; } + /** + * If you don't have a context. Could return null. + */ public static AppCache getInstance() { return _instance; } @@ -121,7 +124,7 @@ public class AppCache { } /** - * Return an abolute file path for any cached content in question. + * Return an absolute file path for any cached content in question. * The file may or may not exist, and it may be deleted at any time. * @param key no fragment allowed */