diff --git a/AndroidManifest.xml b/AndroidManifest.xml index cd0494739c9c563038df6b3aabf3c705ebe16491..8c3d32be53a42b3972011d13702d059f13721971 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.i2p.android.router" - android.versionCode="0" - android.versionName="0.0.0" + android.versionCode="4206788" + android.versionName="0.8.6-24_b132-SDK3" android:installLocation="preferExternal" > <uses-permission android:name="android.permission.INTERNET" /> diff --git a/build.xml b/build.xml index 1adfe98a56d73aeaaa623fd73ac260a46efd6bd7..1d9789711a1523b36f078de05ee5cdf9774b75f7 100644 --- a/build.xml +++ b/build.xml @@ -78,7 +78,10 @@ <!-- overrides of those in main_rules.xml --> - <target name="-pre-build" depends="copy-i2p-resources, incrementBuild" /> + <target name="-pre-build" depends="copy-i2p-resources, incrementBuild" > + <!-- aapt messes up when resources are added or deleted, just build every time --> + <delete dir="${gen.absolute.dir}/net" verbose="${verbose}" /> + </target> <target name="-pre-compile" depends="buildrouter" /> diff --git a/res/layout/main.xml b/res/layout/main.xml index 2fc2a9a166db2075b77f36b1d5c5b18398f8ebef..8a8b089d242f579c0b62ab84f89ef10f732a493c 100644 --- a/res/layout/main.xml +++ b/res/layout/main.xml @@ -7,7 +7,7 @@ <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="I2P News Page" + android:text="I2P Main Page" /> <ImageView android:layout_width="fill_parent" diff --git a/res/layout/news.xml b/res/layout/news.xml index ff88924ab0325f20e237ca544c1af1b62b42bada..8b640536c628e6eff46cef73be434061bc6029be 100644 --- a/res/layout/news.xml +++ b/res/layout/news.xml @@ -5,14 +5,11 @@ android:layout_height="fill_parent" > <TextView + android:id="@+id/news_status" android:layout_width="fill_parent" android:layout_height="wrap_content" - android:text="Hello World, I2PAndroid" - /> -<ImageView - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:src="@drawable/i2plogo" + android:layout_centerHorizontal="true" + android:text="Latest I2P News" /> <WebView android:id="@+id/news_webview" diff --git a/res/raw/router_config b/res/raw/router_config index c6fd9cba6ad359dd433b8257b2cffb2af7a2eadc..a48e139e0bb56297bdb128ec12bd9b8b47cef1db 100644 --- a/res/raw/router_config +++ b/res/raw/router_config @@ -1,7 +1,7 @@ # initial router.config -# temp directory -i2p.dir.temp=/data/data/net.i2p.android.router/files/tmp -i2p.dir.pid=/data/data/net.i2p.android.router/files/tmp +# temp directory now set in Init.java +#i2p.dir.temp=/data/data/net.i2p.android.router/files/tmp +#i2p.dir.pid=/data/data/net.i2p.android.router/files/tmp # save memory prng.buffers=2 prng.bufferSize=32768 diff --git a/src/net/i2p/android/apps/NewsFetcher.java b/src/net/i2p/android/apps/NewsFetcher.java index b69f05977200f36f697b310eb37a5b77751ea30a..6ce2af50e8308a13ac62888823a4161cb446b92f 100644 --- a/src/net/i2p/android/apps/NewsFetcher.java +++ b/src/net/i2p/android/apps/NewsFetcher.java @@ -29,8 +29,21 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { private boolean _invalidated; private File _newsFile; private File _tempFile; + private static NewsFetcher _instance; - private static final String NEWS_FILE = "docs/news.xml"; + public static final NewsFetcher getInstance() { + return _instance; + } + + public static final synchronized NewsFetcher getInstance(RouterContext ctx) { + if (_instance != null) + return _instance; + _instance = new NewsFetcher(ctx); + return _instance; + } + + private static final String NEWS_DIR = "docs"; + private static final String NEWS_FILE = "news.xml"; private static final String TEMP_NEWS_FILE = "news.xml.temp"; /** @since 0.7.14 not configurable */ private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml"; @@ -48,7 +61,10 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { if (last != null) _lastFetch = Long.parseLong(last); } catch (NumberFormatException nfe) {} - _newsFile = new File(_context.getRouterDir(), NEWS_FILE); + File newsDir = new File(_context.getRouterDir(), NEWS_DIR); + // isn't already there on android + newsDir.mkdir(); + _newsFile = new File(newsDir, NEWS_FILE); _tempFile = new File(_context.getTempDir(), TEMP_NEWS_FILE); updateLastFetched(); } @@ -86,15 +102,23 @@ public class NewsFetcher implements Runnable, EepGet.StatusListener { } private static final long INITIAL_DELAY = 5*60*1000; - private static final long RUN_DELAY = 10*60*1000; + private static final long RUN_DELAY = 30*60*1000; public void run() { - try { Thread.sleep(INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY)); } catch (InterruptedException ie) {} + try { + Thread.sleep(INITIAL_DELAY); + } catch (InterruptedException ie) { + return; + } while (true) { if (shouldFetchNews()) { fetchNews(); } - try { Thread.sleep(RUN_DELAY); } catch (InterruptedException ie) {} + try { + Thread.sleep(RUN_DELAY); + } catch (InterruptedException ie) { + break; + } } } diff --git a/src/net/i2p/android/router/activity/NewsActivity.java b/src/net/i2p/android/router/activity/NewsActivity.java index 19ed5f0fbaa2705d8937c4612fb07335c947b8d9..bfee330c8414c9d5d177fbe4acace4e7f74b78ac 100644 --- a/src/net/i2p/android/router/activity/NewsActivity.java +++ b/src/net/i2p/android/router/activity/NewsActivity.java @@ -2,39 +2,81 @@ package net.i2p.android.router.activity; import android.content.res.Resources; import android.os.Bundle; +import android.view.KeyEvent; import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.TextView; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; import net.i2p.android.router.R; +import net.i2p.android.apps.NewsFetcher; public class NewsActivity extends I2PActivityBase { + private long _lastChanged; + + // TODO add some inline style + private static final String HEADER = "<html><head></head><body>"; + private static final String FOOTER = "</body></html>"; + private static final String ERROR_EEPSITE = HEADER + "Sorry, eepsites not yet supported" + FOOTER; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.news); WebView wv = (WebView) findViewById(R.id.news_webview); - if (wv == null) { - System.err.println("No webview resource!"); - return; + wv.setWebViewClient(new NewsWebViewClient()); + } + + @Override + public void onResume() + { + super.onResume(); + NewsFetcher nf = NewsFetcher.getInstance(); + if (nf != null) { + // always update the text + TextView tv = (TextView) findViewById(R.id.news_status); + tv.setText(nf.status().replace(" ", " ")); } + // only update the webview if we need to + File newsFile = new File(_myDir, "docs/news.xml"); + boolean newsExists = newsFile.exists(); + if (_lastChanged > 0 && ((!newsExists) || newsFile.lastModified() < _lastChanged)) + return; + _lastChanged = System.currentTimeMillis(); + + WebView wv = (WebView) findViewById(R.id.news_webview); + InputStream in = null; - ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + ByteArrayOutputStream out = new ByteArrayOutputStream(2048); byte buf[] = new byte[1024]; try { - in = getResources().openRawResource(R.raw.initialnews_html); + if (newsExists) { + out.write(HEADER.getBytes()); + in = new FileInputStream(newsFile); + } else { + in = getResources().openRawResource(R.raw.initialnews_html); + } int read = 0; while ( (read = in.read(buf)) != -1) out.write(buf, 0, read); + + if (newsExists) + out.write(FOOTER.getBytes()); } catch (IOException ioe) { + System.err.println("news error " + ioe); } catch (Resources.NotFoundException nfe) { } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} @@ -45,4 +87,44 @@ public class NewsActivity extends I2PActivityBase { } catch (UnsupportedEncodingException uee) { } } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + WebView wv = (WebView) findViewById(R.id.news_webview); + if ((keyCode == KeyEvent.KEYCODE_BACK) && wv.canGoBack()) { + wv.goBack(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + private static class NewsWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + System.err.println("Should override? " + url); + try { + URI uri = new URI(url); + String s = uri.getScheme(); + if (s == null) + return false; + s = s.toLowerCase(); + if (!(s.equals("http") || s.equals("https"))) + return false; + String h = uri.getHost(); + if (h == null) + return false; + h = h.toLowerCase(); + if (h.endsWith(".i2p")) { + // if (s.equals("https") + // return false; + view.loadData(ERROR_EEPSITE, "text/html", "UTF-8"); + } else { + view.loadUrl(url); + } + return true; + } catch (URISyntaxException use) { + return false; + } + } + } } diff --git a/src/net/i2p/android/router/receiver/I2PReceiver.java b/src/net/i2p/android/router/receiver/I2PReceiver.java index 274b36419ba245aabec4a962f1c3870ade7db5ce..f9f5bd1f33fcd33e1c499020573e04241b3d67b8 100644 --- a/src/net/i2p/android/router/receiver/I2PReceiver.java +++ b/src/net/i2p/android/router/receiver/I2PReceiver.java @@ -17,6 +17,7 @@ public class I2PReceiver extends BroadcastReceiver { private final Context _context; private boolean _isBound; private RouterService _routerService; + private ServiceConnection _connection; /** * Registers itself @@ -28,8 +29,6 @@ public class I2PReceiver extends BroadcastReceiver { IntentFilter intents = new IntentFilter(); intents.addAction(Intent.ACTION_TIME_CHANGED); intents.addAction(Intent.ACTION_TIME_TICK); // once per minute, for testing - intents.addAction(Intent.ACTION_SCREEN_OFF); - intents.addAction(Intent.ACTION_SCREEN_ON); intents.addAction(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(this, intents); boolean success = bindRouter(); @@ -90,11 +89,17 @@ public class I2PReceiver extends BroadcastReceiver { Intent intent = new Intent(); intent.setClassName(_context, "net.i2p.android.router.service.RouterService"); System.err.println(this + " calling bindService"); - boolean success = _context.bindService(intent, new RouterConnection(), Context.BIND_AUTO_CREATE); + _connection = new RouterConnection(); + boolean success = _context.bindService(intent, _connection, Context.BIND_AUTO_CREATE); System.err.println(this + " got from bindService: " + success); return success; } + public void unbindRouter() { + if (_connection != null) + _context.unbindService(_connection); + } + private class RouterConnection implements ServiceConnection { public void onServiceConnected(ComponentName name, IBinder service) { diff --git a/src/net/i2p/android/router/service/Init.java b/src/net/i2p/android/router/service/Init.java index baaa88841580469a4a5bf3723d875c20c3f49e9d..7eb98ef5b464e61281359f072318909dae937c7a 100644 --- a/src/net/i2p/android/router/service/Init.java +++ b/src/net/i2p/android/router/service/Init.java @@ -73,11 +73,14 @@ class Init { } void initialize() { - mergeResourceToFile(R.raw.router_config, "router.config"); - mergeResourceToFile(R.raw.logger_config, "logger.config"); - mergeResourceToFile(R.raw.i2ptunnel_config, "i2ptunnel.config"); + Properties props = new Properties(); + props.setProperty("i2p.dir.temp", myDir + "/tmp"); + props.setProperty("i2p.dir.pid", myDir + "/tmp"); + mergeResourceToFile(R.raw.router_config, "router.config", props); + mergeResourceToFile(R.raw.logger_config, "logger.config", null); + mergeResourceToFile(R.raw.i2ptunnel_config, "i2ptunnel.config", null); // FIXME this is a memory hog to merge this way - mergeResourceToFile(R.raw.hosts_txt, "hosts.txt"); + mergeResourceToFile(R.raw.hosts_txt, "hosts.txt", null); copyResourceToFile(R.raw.blocklist_txt, "blocklist.txt"); // Set up the locations so Router and WorkingDir can find them @@ -115,8 +118,9 @@ class Init { * and write back * For now, do it backwards so we can override with new apks. * When we have user configurable stuff, switch it back. + * @param props local overrides or null */ - private void mergeResourceToFile(int resID, String f) { + private void mergeResourceToFile(int resID, String f, Properties overrides) { InputStream in = null; InputStream fin = null; @@ -138,6 +142,8 @@ class Init { // override user settings DataHelper.loadProps(props, in); + if (overrides != null) + props.putAll(overrides); DataHelper.storeProps(props, ctx.getFileStreamPath(f)); } catch (IOException ioe) { } catch (Resources.NotFoundException nfe) { diff --git a/src/net/i2p/android/router/service/LoadClientsJob.java b/src/net/i2p/android/router/service/LoadClientsJob.java index 21180f4d173450af202490720195b830fb91f9ed..e74bf744e787e7ab4879641518f75a0dd28262fe 100644 --- a/src/net/i2p/android/router/service/LoadClientsJob.java +++ b/src/net/i2p/android/router/service/LoadClientsJob.java @@ -1,10 +1,11 @@ package net.i2p.android.router.service; +import net.i2p.android.apps.NewsFetcher; import net.i2p.i2ptunnel.TunnelControllerGroup; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; -import net.i2p.util.I2PThread; +import net.i2p.util.I2PAppThread; /** * Load the clients we want. @@ -27,6 +28,8 @@ import net.i2p.util.I2PThread; */ class LoadClientsJob extends JobImpl { + private Thread _fetcherThread; + /** this is the delay to load the clients. There are additional delays e.g. in i2ptunnel.config */ private static final long LOAD_DELAY = 10*1000; @@ -41,10 +44,15 @@ class LoadClientsJob extends JobImpl { public void runJob() { Job j = new RunI2PTunnel(getContext()); getContext().jobQueue().addJob(j); + + NewsFetcher fetcher = NewsFetcher.getInstance(getContext()); + _fetcherThread = new I2PAppThread(fetcher, "NewsFetcher", true); + _fetcherThread.start(); + // add other clients here } - private static class RunI2PTunnel extends JobImpl { + private class RunI2PTunnel extends JobImpl { public RunI2PTunnel(RouterContext ctx) { super(ctx); @@ -61,9 +69,11 @@ class LoadClientsJob extends JobImpl { } } - private static class I2PTunnelShutdownHook implements Runnable { + private class I2PTunnelShutdownHook implements Runnable { public void run() { System.err.println("i2ptunnel shutdown hook"); + if (_fetcherThread != null) + _fetcherThread.interrupt(); TunnelControllerGroup.getInstance().unloadControllers(); } } diff --git a/src/net/i2p/android/router/service/RouterService.java b/src/net/i2p/android/router/service/RouterService.java index ed998663953e3ba6a750b35fed7f029e97ee5125..a9f27ff72d6999aaa5375fd9e556c2251dc59231 100644 --- a/src/net/i2p/android/router/service/RouterService.java +++ b/src/net/i2p/android/router/service/RouterService.java @@ -201,6 +201,9 @@ public class RouterService extends Service { // ******** following methods may be accessed from Activities and Receivers ************ + /** + * @returns null if router is not running + */ public RouterContext getRouterContext() { RouterContext rv = _context; if (rv == null) @@ -215,10 +218,35 @@ public class RouterService extends Service { return rv; } + /** + * Stop and don't restart + */ public void manualStop() { + synchronized (_stateLock) { + if (_state == State.WAITING || _state == State.STARTING) + _starterThread.interrupt(); + if (_state == State.STARTING || _state == State.RUNNING) { + _statusBar.update("I2P is stopping"); + Thread stopperThread = new Thread(new Stopper(State.MANUAL_STOPPING, State.MANUAL_STOPPED)); + stopperThread.start(); + } + } } + /** + * Stop and then spin waiting for a network connection, then restart + */ public void networkStop() { + synchronized (_stateLock) { + if (_state == State.WAITING || _state == State.STARTING) + _starterThread.interrupt(); + if (_state == State.STARTING || _state == State.RUNNING) { + _statusBar.update("I2P is stopping"); + // don't change state, let the shutdown hook do it + Thread stopperThread = new Thread(new Stopper(State.NETWORK_STOPPING, State.NETWORK_STOPPING)); + stopperThread.start(); + } + } } public void restart() { @@ -231,29 +259,39 @@ public class RouterService extends Service { System.err.println("onDestroy called" + "Current state is: " + _state); - BroadcastReceiver rcvr = _receiver; + _statusBar.off(this); + + I2PReceiver rcvr = _receiver; if (rcvr != null) { synchronized(rcvr) { unregisterReceiver(rcvr); + rcvr.unbindRouter(); _receiver = null; } } synchronized (_stateLock) { - if (_state == State.STARTING) + if (_state == State.WAITING || _state == State.STARTING) _starterThread.interrupt(); if (_state == State.STARTING || _state == State.RUNNING) { - _state = State.STOPPING; // should this be in a thread? _statusBar.update("I2P is stopping"); - Thread stopperThread = new Thread(new Stopper()); + Thread stopperThread = new Thread(new Stopper(State.STOPPING, State.STOPPED)); stopperThread.start(); - } else if (_state != State.STOPPING) { - _statusBar.off(this); } } } private class Stopper implements Runnable { + private final State nextState; + private final State stopState; + + /** call holding statelock */ + public Stopper(State next, State stop) { + nextState = next; + stopState = stop; + _state = next; + } + public void run() { System.err.println(MARKER + this + " stopper thread" + "Current state is: " + _state); @@ -262,7 +300,8 @@ public class RouterService extends Service { _statusBar.off(RouterService.this); System.err.println("shutdown complete"); synchronized (_stateLock) { - _state = State.STOPPED; + if (_state == nextState) + _state = stopState; } } } @@ -272,15 +311,26 @@ public class RouterService extends Service { System.err.println(this + " shutdown hook" + "Current state is: " + _state); _statusBar.off(RouterService.this); - BroadcastReceiver rcvr = _receiver; + I2PReceiver rcvr = _receiver; if (rcvr != null) { synchronized(rcvr) { unregisterReceiver(rcvr); + rcvr.unbindRouter(); _receiver = null; } } synchronized (_stateLock) { - if (_state == State.STARTING || _state == State.RUNNING) { + if (_state == State.WAITING || _state == State.STARTING) + _starterThread.interrupt(); + if (_state == State.MANUAL_STOPPING) { + _state = State.MANUAL_STOPPED; + } else if (_state == State.NETWORK_STOPPING) { + // start waiter thread + _state = State.WAITING; + _starterThread = new Thread(new Waiter()); + _starterThread.start(); + } else if (_state == State.STARTING || _state == State.RUNNING || + _state == State.STOPPING) { _state = State.STOPPED; if (_statusThread != null) _statusThread.interrupt();