diff --git a/AndroidManifest.xml.in b/AndroidManifest.xml.in
index bcc27cb9bc8b643bf35005adfc89f56cbf9cfeff..c8060481378c54209b315f02a96a73ffc2e4d299 100644
--- a/AndroidManifest.xml.in
+++ b/AndroidManifest.xml.in
@@ -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-0_b0-API8"
+      android:versionCode="0"
+      android:versionName="0.0.0-0_b0-API8"
       android:installLocation="auto"
       >
     <uses-permission android:name="android.permission.INTERNET" />
@@ -32,11 +32,17 @@
                   android:label="I2P News"
                   android:configChanges="orientation|keyboardHidden" >
         </activity>
-        <activity android:name=".activity.TextResourceActivity"
-                  android:label="I2P Information" >
+        <activity android:name=".activity.HelpActivity"
+                  android:label="Help"
+                  android:parentActivityName=".activity.MainActivity" >
         </activity>
         <activity android:name=".activity.LicenseActivity"
-                  android:label="I2P License Information" >
+                  android:label="I2P License Information"
+                  android:parentActivityName=".activity.HelpActivity" >
+        </activity>
+        <activity android:name=".activity.PeersActivity"
+                  android:label="Peers Information"
+                  android:parentActivityName=".activity.MainActivity" >
         </activity>
         <activity android:name=".activity.WebActivity"
                   android:label="I2P Web Browser"
@@ -50,18 +56,42 @@
         </activity>
         <activity android:name=".activity.SettingsActivity"
                   android:label="I2P Settings"
+                  android:parentActivityName=".activity.MainActivity"
                   android:launchMode="singleTop" >
         </activity>
         <activity android:name=".activity.AddressbookSettingsActivity"
                   android:label="I2P Addressbook Settings"
+                  android:parentActivityName=".activity.AddressbookActivity"
                   android:launchMode="singleTop" >
         </activity>
         <activity android:name=".activity.AddressbookActivity"
                   android:label="Addressbook"
                   android:launchMode="singleTop" >
+            <intent-filter>
+                <action android:name="android.intent.action.SEARCH" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.PICK" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="android.app.searchable"
+                       android:resource="@xml/searchable_addressbook"/>
+        </activity>
+        <activity android:name="net.i2p.android.i2ptunnel.activity.TunnelListActivity"
+                  android:label="I2PTunnel"
+                  android:launchMode="singleTop" >
+        </activity>
+        <activity android:name="net.i2p.android.i2ptunnel.activity.TunnelDetailActivity"
+                  android:label="I2PTunnel"
+                  android:parentActivityName="net.i2p.android.i2ptunnel.activity.TunnelListActivity" >
+        </activity>
+        <activity android:name="net.i2p.android.i2ptunnel.activity.TunnelWizardActivity"
+                  android:label="Tunnel Creation Wizard"
+                  android:parentActivityName="net.i2p.android.i2ptunnel.activity.TunnelListActivity" >
         </activity>
         <activity android:name=".activity.LogActivity"
-                  android:label="I2P Logs" >
+                  android:label="I2P Logs"
+                  android:parentActivityName=".activity.MainActivity" >
         </activity>
         <activity android:name=".activity.PeersActivity"
                   android:label="I2P Peers and Transport Status"
diff --git a/custom_rules.xml b/custom_rules.xml
index f582dafa514a91d3d6d68f817c36ac654ec118b5..d2af07c3a082bd042153b998920a218f65f4c85f 100644
--- a/custom_rules.xml
+++ b/custom_rules.xml
@@ -247,10 +247,10 @@
                 props.store(fo, "AUTO-GENERATED FILE, DO NOT EDIT!");
                 fo.close();
             }
-            sp1 = 'android.versionCode="[0-9]*"';
-            rp1 = 'android.versionCode="' + props.getProperty("my.version.code") + '"';
-            sp2 = 'android.versionName="[^"]*"';
-            rp2 = 'android.versionName="' + props.getProperty("my.version.name") + '"';
+            sp1 = 'android:versionCode="[0-9]*"';
+            rp1 = 'android:versionCode="' + props.getProperty("my.version.code") + '"';
+            sp2 = 'android:versionName="[^"]*"';
+            rp2 = 'android:versionName="' + props.getProperty("my.version.name") + '"';
 
             /* sed primary manifest */
             cfg = File(attributes.get("mainxml") + ".in");
diff --git a/res/drawable-hdpi/drawer_shadow.9.png b/res/drawable-hdpi/drawer_shadow.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..224cc4ff43a29c546ae50c654c20c58c2f4cdb75
Binary files /dev/null and b/res/drawable-hdpi/drawer_shadow.9.png differ
diff --git a/res/drawable-hdpi/ic_action_search.png b/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..f12e005ebe835c1dd2f6ae324224c3ee296d2d68
Binary files /dev/null and b/res/drawable-hdpi/ic_action_search.png differ
diff --git a/res/drawable-hdpi/ic_av_play.png b/res/drawable-hdpi/ic_av_play.png
new file mode 100644
index 0000000000000000000000000000000000000000..df8a2ca28e1e3978a59dbe9cf415a59429357084
Binary files /dev/null and b/res/drawable-hdpi/ic_av_play.png differ
diff --git a/res/drawable-hdpi/ic_av_stop.png b/res/drawable-hdpi/ic_av_stop.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd5d6a1c1bd14a1222647db5f2cc993e9f2c63f1
Binary files /dev/null and b/res/drawable-hdpi/ic_av_stop.png differ
diff --git a/res/drawable-hdpi/ic_content_discard.png b/res/drawable-hdpi/ic_content_discard.png
new file mode 100644
index 0000000000000000000000000000000000000000..ffd19d9e80b07c6fdf216f2b728a9337e438d1c4
Binary files /dev/null and b/res/drawable-hdpi/ic_content_discard.png differ
diff --git a/res/drawable-hdpi/ic_content_edit.png b/res/drawable-hdpi/ic_content_edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..3eb5190fb44de54efc5a1675090aeedb25b1d0c6
Binary files /dev/null and b/res/drawable-hdpi/ic_content_edit.png differ
diff --git a/res/drawable-hdpi/ic_drawer.png b/res/drawable-hdpi/ic_drawer.png
new file mode 100644
index 0000000000000000000000000000000000000000..ff7b1def9ac3f86488a855f502b965ac75b633fb
Binary files /dev/null and b/res/drawable-hdpi/ic_drawer.png differ
diff --git a/res/drawable-hdpi/ic_navigation_refresh.png b/res/drawable-hdpi/ic_navigation_refresh.png
new file mode 100644
index 0000000000000000000000000000000000000000..bb9d855f77692343e173f814ca1e546393b707d8
Binary files /dev/null and b/res/drawable-hdpi/ic_navigation_refresh.png differ
diff --git a/res/drawable-mdpi/drawer_shadow.9.png b/res/drawable-mdpi/drawer_shadow.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..3797f99c0ef9f657c2b0a1f84a4d0f16cc8ee5f4
Binary files /dev/null and b/res/drawable-mdpi/drawer_shadow.9.png differ
diff --git a/res/drawable-mdpi/ic_action_search.png b/res/drawable-mdpi/ic_action_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..587d9e0bf392fc928947f04293ba009f7fc77b29
Binary files /dev/null and b/res/drawable-mdpi/ic_action_search.png differ
diff --git a/res/drawable-mdpi/ic_av_play.png b/res/drawable-mdpi/ic_av_play.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a40cd5f7bff6c57d8349bcba17648e2fdabedc4
Binary files /dev/null and b/res/drawable-mdpi/ic_av_play.png differ
diff --git a/res/drawable-mdpi/ic_av_stop.png b/res/drawable-mdpi/ic_av_stop.png
new file mode 100644
index 0000000000000000000000000000000000000000..20df415869bf4c224f577abc6fc30f107cada685
Binary files /dev/null and b/res/drawable-mdpi/ic_av_stop.png differ
diff --git a/res/drawable-mdpi/ic_content_discard.png b/res/drawable-mdpi/ic_content_discard.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8ee5f253f83e715845337278cb7d08ebd623dfa
Binary files /dev/null and b/res/drawable-mdpi/ic_content_discard.png differ
diff --git a/res/drawable-mdpi/ic_content_edit.png b/res/drawable-mdpi/ic_content_edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a28d032248db6a24a68edcfba82dce145c473f3
Binary files /dev/null and b/res/drawable-mdpi/ic_content_edit.png differ
diff --git a/res/drawable-mdpi/ic_drawer.png b/res/drawable-mdpi/ic_drawer.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb681ba2639897cc4646d3784b97bbe16f5d4e91
Binary files /dev/null and b/res/drawable-mdpi/ic_drawer.png differ
diff --git a/res/drawable-mdpi/ic_navigation_refresh.png b/res/drawable-mdpi/ic_navigation_refresh.png
new file mode 100644
index 0000000000000000000000000000000000000000..bd611e8e24d2e211a94d1658a49c823c4201716f
Binary files /dev/null and b/res/drawable-mdpi/ic_navigation_refresh.png differ
diff --git a/res/drawable-xhdpi/drawer_shadow.9.png b/res/drawable-xhdpi/drawer_shadow.9.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa3d853e902401d850f09b1cc50a58cda9bd3bb5
Binary files /dev/null and b/res/drawable-xhdpi/drawer_shadow.9.png differ
diff --git a/res/drawable-xhdpi/ic_action_search.png b/res/drawable-xhdpi/ic_action_search.png
new file mode 100644
index 0000000000000000000000000000000000000000..3549f84dd8f5e38665849b46e252bc34f29be027
Binary files /dev/null and b/res/drawable-xhdpi/ic_action_search.png differ
diff --git a/res/drawable-xhdpi/ic_av_play.png b/res/drawable-xhdpi/ic_av_play.png
new file mode 100644
index 0000000000000000000000000000000000000000..51124993df109972aa504c796b17e788a3ed0efd
Binary files /dev/null and b/res/drawable-xhdpi/ic_av_play.png differ
diff --git a/res/drawable-xhdpi/ic_av_stop.png b/res/drawable-xhdpi/ic_av_stop.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee5eda25c7c059489d86e8204ab96c9dc026df9f
Binary files /dev/null and b/res/drawable-xhdpi/ic_av_stop.png differ
diff --git a/res/drawable-xhdpi/ic_content_discard.png b/res/drawable-xhdpi/ic_content_discard.png
new file mode 100644
index 0000000000000000000000000000000000000000..412b33354138bab210c8814dff6e20a8a49d7cfd
Binary files /dev/null and b/res/drawable-xhdpi/ic_content_discard.png differ
diff --git a/res/drawable-xhdpi/ic_content_edit.png b/res/drawable-xhdpi/ic_content_edit.png
new file mode 100644
index 0000000000000000000000000000000000000000..4215579aedd5edf6f5314de15e7b9175eb5acff2
Binary files /dev/null and b/res/drawable-xhdpi/ic_content_edit.png differ
diff --git a/res/drawable-xhdpi/ic_drawer.png b/res/drawable-xhdpi/ic_drawer.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9bc3d70f1d29fec2e4530e5d1809edc8e15ad35
Binary files /dev/null and b/res/drawable-xhdpi/ic_drawer.png differ
diff --git a/res/drawable-xhdpi/ic_navigation_refresh.png b/res/drawable-xhdpi/ic_navigation_refresh.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7fdc0dfcb1dc50bd0ebd2527d174398b8470797
Binary files /dev/null and b/res/drawable-xhdpi/ic_navigation_refresh.png differ
diff --git a/res/drawable/ic_menu_play_clip.png b/res/drawable/ic_menu_play_clip.png
deleted file mode 100644
index 466994744c9d60fdcc1697daa63b37879fda80ae..0000000000000000000000000000000000000000
Binary files a/res/drawable/ic_menu_play_clip.png and /dev/null differ
diff --git a/res/drawable/ic_menu_refresh.png b/res/drawable/ic_menu_refresh.png
deleted file mode 100644
index 77d70dd4f0534271b71ef4eb87f5a7a917d944fa..0000000000000000000000000000000000000000
Binary files a/res/drawable/ic_menu_refresh.png and /dev/null differ
diff --git a/res/drawable/local_down.png b/res/drawable/local_down.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d056de5da5e89dc55609522bfc9224526665236
Binary files /dev/null and b/res/drawable/local_down.png differ
diff --git a/res/drawable/local_inprogress.png b/res/drawable/local_inprogress.png
new file mode 100644
index 0000000000000000000000000000000000000000..245dc058d8274486f073b53ed73763e97d0e0af5
Binary files /dev/null and b/res/drawable/local_inprogress.png differ
diff --git a/res/drawable/local_up.png b/res/drawable/local_up.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b8a3451707baef478833ea6a3411a523bea0661
Binary files /dev/null and b/res/drawable/local_up.png differ
diff --git a/res/layout/activity_addressbook.xml b/res/layout/activity_addressbook.xml
deleted file mode 100644
index 4c7dc6b684ba156f142e96862ecde1e1027c7e08..0000000000000000000000000000000000000000
--- a/res/layout/activity_addressbook.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/addressbook_list"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent" >
-    
-
-</ListView>
diff --git a/res/layout/activity_navdrawer_onepane.xml b/res/layout/activity_navdrawer_onepane.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3ee46906f74c25c089ff752d930978dfd6288f25
--- /dev/null
+++ b/res/layout/activity_navdrawer_onepane.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/drawer_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity" >
+
+    <!-- The main content view -->
+    <FrameLayout
+        android:id="@+id/main_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <!-- The navigation drawer -->
+    <ListView
+        android:id="@+id/drawer"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:choiceMode="singleChoice"
+        android:divider="@android:color/transparent"
+        android:dividerHeight="0dp"
+        android:background="#111"/>
+
+</android.support.v4.widget.DrawerLayout>
\ No newline at end of file
diff --git a/res/layout/activity_navdrawer_twopane.xml b/res/layout/activity_navdrawer_twopane.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eb11f12cfb935422d738de387c02916c5840c3ae
--- /dev/null
+++ b/res/layout/activity_navdrawer_twopane.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/drawer_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:baselineAligned="false"
+        android:orientation="horizontal" >
+
+        <!--
+        This layout is a two-pane layout for the master/detail
+        flow within a DrawerLayout. See res/values-large/refs.xml
+        and res/values-sw600dp/refs.xml for layout aliases
+        that replace the single-pane version of the layout with
+        this two-pane version.
+        -->
+
+        <!-- The main fragment view -->
+        <FrameLayout
+            android:id="@+id/main_fragment"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1" />
+
+        <!-- The detail fragment view -->
+        <FrameLayout
+            android:id="@+id/detail_fragment"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="2" />
+
+    </LinearLayout>
+
+    <!-- The navigation drawer -->
+    <ListView
+        android:id="@+id/drawer"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:choiceMode="singleChoice"
+        android:divider="@android:color/transparent"
+        android:dividerHeight="0dp"
+        android:background="#111"/>
+
+</android.support.v4.widget.DrawerLayout>
\ No newline at end of file
diff --git a/res/layout/activity_wizard.xml b/res/layout/activity_wizard.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9d03ac281e4fb1a9a67aab40091ee6e5289c71a9
--- /dev/null
+++ b/res/layout/activity_wizard.xml
@@ -0,0 +1,47 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <net.i2p.android.wizard.ui.StepPagerStrip
+        android:id="@+id/strip"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="16dp"
+        android:layout_marginRight="16dp"
+        android:paddingTop="16dp"
+        android:paddingBottom="8dp"
+        android:gravity="fill_horizontal" />
+
+    <android.support.v4.view.ViewPager
+        android:id="@+id/pager"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <View android:id="@+id/divider"
+        android:layout_height="1dp"
+        android:layout_width="match_parent" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/prev_button"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:text="@string/prev" />
+
+        <Button
+            android:id="@+id/next_button"
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:text="@string/next" />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/addressbook_header.xml b/res/layout/addressbook_header.xml
deleted file mode 100644
index b787646022e704c1362d390a295fc22142a9b684..0000000000000000000000000000000000000000
--- a/res/layout/addressbook_header.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="wrap_content" 
-    android:padding="8dp"
-    android:textSize="18sp"
-    android:text="Address Book" >
-</TextView>
diff --git a/res/layout/fragment_i2ptunnel_detail.xml b/res/layout/fragment_i2ptunnel_detail.xml
new file mode 100644
index 0000000000000000000000000000000000000000..97a5b32494d66dc2827a60278e09d79b3ffaa5ac
--- /dev/null
+++ b/res/layout/fragment_i2ptunnel_detail.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <TextView
+        android:id="@+id/tunnel_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:padding="3dp"
+        android:text="Tunnel name"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <TextView
+        android:id="@+id/tunnel_description"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_below="@+id/tunnel_type"
+        android:text="Tunnel description" />
+
+    <TextView
+        android:id="@+id/tunnel_target_interface_port"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_below="@+id/tunnel_description"
+        android:gravity="right"
+        android:text="Interface:port" />
+
+    <TextView
+        android:id="@+id/textView2"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_below="@+id/tunnel_name"
+        android:text="Type:" />
+
+    <TextView
+        android:id="@+id/textView1"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_below="@+id/textView2"
+        android:text="Description:" />
+
+    <TextView
+        android:id="@+id/tunnel_type"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_below="@+id/tunnel_name"
+        android:text="Tunnel type" />
+
+    <TextView
+        android:id="@+id/textView3"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_below="@+id/textView1"
+        android:text="Target:" />
+
+    <TextView
+        android:id="@+id/textView4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_below="@+id/textView3"
+        android:text="Access point:" />
+
+    <TextView
+        android:id="@+id/tunnel_access_interface_port"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignBaseline="@+id/textView4"
+        android:layout_alignBottom="@+id/textView4"
+        android:layout_alignParentRight="true"
+        android:gravity="right"
+        android:text="Interface:port" />
+
+    <CheckBox
+        android:id="@+id/tunnel_autostart"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentTop="true"
+        android:clickable="false"
+        android:text="Auto start" />
+
+</RelativeLayout>
diff --git a/res/layout/main.xml b/res/layout/fragment_main.xml
similarity index 67%
rename from res/layout/main.xml
rename to res/layout/fragment_main.xml
index 8cf86486b7ad2ae43cdf1b28acb90f308f505795..9188c47718856660a3ec769e4db7f81bc5c9972d 100644
--- a/res/layout/main.xml
+++ b/res/layout/fragment_main.xml
@@ -3,7 +3,7 @@
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:fillViewport="true"
-    android:padding="10px"
+    android:padding="10dp"
     android:scrollbarStyle="outsideInset"
     >
 <LinearLayout
@@ -13,6 +13,7 @@
     >
 
     <ImageView
+        android:contentDescription="@string/desc_i2p_logo"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:src="@drawable/i2plogo" />
@@ -58,41 +59,6 @@
     android:drawableLeft="@drawable/stop_router_icon" />
 -->
 
-<TextView
-    android:id="@+id/textView2"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:text="@string/label_tools"
-    android:textAppearance="?android:attr/textAppearanceMedium" />
-
-<Button
-    android:id="@+id/addressbook_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_addressbook"
-    android:drawableLeft="@drawable/addressbook_icon" />
-
-<Button
-    android:id="@+id/logs_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_logs"
-    android:drawableLeft="@drawable/log_icon" />
-
-<Button
-    android:id="@+id/error_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_error_logs"
-    android:drawableLeft="@drawable/error_log_icon" />
-
-<Button
-    android:id="@+id/peers_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_peers_status"
-    android:drawableLeft="@drawable/peers_icon" />
-
 <TextView
     android:id="@+id/textView3"
     android:layout_width="wrap_content"
@@ -114,20 +80,6 @@
     android:text="@string/label_news"
     android:drawableLeft="@drawable/information_icon" />
 
-<Button
-    android:id="@+id/releasenotes_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_release_notes"
-    android:drawableLeft="@drawable/information_icon" />
-
-<Button
-    android:id="@+id/licenses_button"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:text="@string/label_licenses"
-    android:drawableLeft="@drawable/information_icon" />
-
 <TextView
     android:id="@+id/textView5"
     android:layout_width="wrap_content"
diff --git a/res/layout/fragment_wizard_page.xml b/res/layout/fragment_wizard_page.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cbc7186b417e27cc8b5785b7eae3402f1d3c7c6c
--- /dev/null
+++ b/res/layout/fragment_wizard_page.xml
@@ -0,0 +1,31 @@
+<!--
+  Copyright 2013 Google Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/WizardPageContainer">
+
+    <TextView style="@style/WizardPageTitle" />
+
+    <ListView android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:saveEnabled="false"
+        android:scrollbarStyle="outsideOverlay" />
+
+</LinearLayout>
diff --git a/res/layout/fragment_wizard_page_single_boolean.xml b/res/layout/fragment_wizard_page_single_boolean.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1ecb5a54d957c211129553d1842c13d8b70980be
--- /dev/null
+++ b/res/layout/fragment_wizard_page_single_boolean.xml
@@ -0,0 +1,45 @@
+<!--
+  Copyright 2013 str4d
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/WizardPageContainer">
+
+    <TextView style="@style/WizardPageTitle" />
+
+    <ScrollView android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:scrollbarStyle="outsideOverlay">
+
+        <LinearLayout android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <CheckBox android:id="@+id/wizard_check_box"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:saveEnabled="false"
+                android:layout_marginBottom="16dp"
+                android:text="@string/enabled" />
+
+            <TextView style="@style/WizardFormLabel"
+                android:id="@+id/wizard_text_field_desc" />
+
+        </LinearLayout>
+    </ScrollView>
+</LinearLayout>
diff --git a/res/layout/fragment_wizard_page_single_text_field.xml b/res/layout/fragment_wizard_page_single_text_field.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9a76c502bc7e0deb99cbbc0e6e7a0550ad5ef402
--- /dev/null
+++ b/res/layout/fragment_wizard_page_single_text_field.xml
@@ -0,0 +1,49 @@
+<!--
+  Copyright 2013 str4d
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/WizardPageContainer">
+
+    <TextView style="@style/WizardPageTitle" />
+
+    <ScrollView android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:scrollbarStyle="outsideOverlay">
+
+        <LinearLayout android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <EditText android:id="@+id/wizard_text_field"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:saveEnabled="false"
+                android:inputType="text"
+                android:layout_marginBottom="16dp" />
+
+            <TextView android:id="@+id/wizard_text_field_feedback"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+            <TextView style="@style/WizardFormLabel"
+                android:id="@+id/wizard_text_field_desc" />
+
+        </LinearLayout>
+    </ScrollView>
+</LinearLayout>
diff --git a/res/layout/fragment_wizard_page_single_text_field_picker.xml b/res/layout/fragment_wizard_page_single_text_field_picker.xml
new file mode 100644
index 0000000000000000000000000000000000000000..321b022e63afa66e73077d9dd57575bfcfa0c156
--- /dev/null
+++ b/res/layout/fragment_wizard_page_single_text_field_picker.xml
@@ -0,0 +1,59 @@
+<!--
+  Copyright 2013 str4d
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/WizardPageContainer">
+
+    <TextView style="@style/WizardPageTitle" />
+
+    <ScrollView android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:paddingLeft="16dp"
+        android:paddingRight="16dp"
+        android:scrollbarStyle="outsideOverlay">
+
+        <RelativeLayout android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <EditText
+                android:id="@+id/wizard_text_field"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentLeft="true"
+                android:layout_marginBottom="16dp"
+                android:layout_toLeftOf="@+id/wizard_text_field_pick"
+                android:inputType="text"
+                android:saveEnabled="false" />
+
+            <Button android:id="@+id/wizard_text_field_pick"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentRight="true"
+                android:text="@string/label_browse" />
+
+            <TextView android:id="@+id/wizard_text_field_feedback"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_below="@+id/wizard_text_field" />
+
+            <TextView style="@style/WizardFormLabel"
+                android:id="@+id/wizard_text_field_desc"
+                android:layout_below="@+id/wizard_text_field_feedback" />
+
+        </RelativeLayout>
+    </ScrollView>
+</LinearLayout>
diff --git a/res/layout/listitem_i2ptunnel.xml b/res/layout/listitem_i2ptunnel.xml
new file mode 100644
index 0000000000000000000000000000000000000000..529bd69f5fededa88a4388970490dee676aedb54
--- /dev/null
+++ b/res/layout/listitem_i2ptunnel.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:padding="5dp" >
+
+    <!-- The name of the tunnel -->
+    <TextView android:id="@+id/tunnel_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="3dp"
+        android:textSize="16sp"
+        android:layout_alignParentLeft="true"
+        android:layout_marginRight="5dp"
+        android:text="Tunnel name" />
+
+    <!-- The type of tunnel -->
+    <TextView android:id="@+id/tunnel_type"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/tunnel_name"
+        android:text="Tunnel type" />
+
+    <!-- Additional tunnel details -->
+    <TextView android:id="@+id/tunnel_details"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/tunnel_type"
+        android:text="Tunnel details" />
+
+    <!-- Interface:port the tunnel listens on or points to -->
+    <TextView android:id="@+id/tunnel_interface_port"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toRightOf="@id/tunnel_name"
+        android:layout_alignParentRight="true"
+        android:layout_alignTop="@id/tunnel_name"
+        android:gravity="right"
+        android:text="Interface:port" />
+
+    <!-- Status star -->
+    <ImageView android:id="@+id/tunnel_status"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:layout_centerVertical="true"
+        android:contentDescription="Status" />
+
+</RelativeLayout>
diff --git a/res/layout/listitem_wizard_review.xml b/res/layout/listitem_wizard_review.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c0fe3a724cb8b682c859075c1706737c6f18393e
--- /dev/null
+++ b/res/layout/listitem_wizard_review.xml
@@ -0,0 +1,40 @@
+<!--
+  Copyright 2013 Google Inc.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:baselineAligned="true"
+    android:paddingTop="12dp"
+    android:paddingBottom="12dp">
+
+    <TextView android:id="@android:id/text1"
+        style="?android:textAppearanceSmall"
+        android:textAllCaps="true"
+        android:textStyle="bold"
+        android:ellipsize="end"
+        android:layout_width="100sp"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="16dp" />
+
+    <TextView android:id="@android:id/text2"
+        style="?android:textAppearanceMedium"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1" />
+
+</LinearLayout>
diff --git a/res/menu/activity_addressbook_actions.xml b/res/menu/activity_addressbook_actions.xml
index 1941cfa9ce05c7aa9e7c59e3b1dbb9df0181cf5a..40447d2be8bb4094774bab8939229cc7e5f405c4 100644
--- a/res/menu/activity_addressbook_actions.xml
+++ b/res/menu/activity_addressbook_actions.xml
@@ -1,12 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:i2pandroid="http://schemas.android.com/apk/res-auto" >
-    <!-- Add, should appear as action buttons --><!--
-    <item android:id="@+id/action_add_to_addressbook"
-          android:title="@string/action_add"
-          android:icon="@drawable/ic_content_new"
-          i2pandroid:showAsAction="ifRoom" />-->
-    <!-- Settings, should always be in the overflow -->
-    <item android:id="@+id/action_addressbook_settings"
-          android:title="@string/menu_settings"
-          i2pandroid:showAsAction="never" /></menu>
+    <!-- Search, should appear as action buttons -->
+    <item android:id="@+id/action_search_addressbook"
+          android:title="@string/action_search"
+          android:icon="@drawable/ic_action_search"
+          i2pandroid:showAsAction="ifRoom|collapseActionView"
+          i2pandroid:actionViewClass="android.support.v7.widget.SearchView" />
+</menu>
diff --git a/res/menu/activity_help_actions.xml b/res/menu/activity_help_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e8e3b68c6201f5d3d590a4c46946671bce23e070
--- /dev/null
+++ b/res/menu/activity_help_actions.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:i2pandroid="http://schemas.android.com/apk/res-auto" >
+    <item android:id="@+id/menu_help_licenses"
+        android:title="@string/label_licenses"
+        i2pandroid:showAsAction="never" />
+    <item android:id="@+id/menu_help_release_notes"
+        android:title="@string/label_release_notes"
+        i2pandroid:showAsAction="never" />
+
+</menu>
diff --git a/res/menu/activity_main_actions.xml b/res/menu/activity_main_actions.xml
new file mode 100755
index 0000000000000000000000000000000000000000..ab07512ef3e0bea97b7f33623e237f853c8ca98a
--- /dev/null
+++ b/res/menu/activity_main_actions.xml
@@ -0,0 +1,12 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:i2pandroid="http://schemas.android.com/apk/res-auto">
+    <item android:title="@string/menu_settings"
+          android:id="@+id/menu_settings"
+          android:icon="@android:drawable/ic_menu_preferences"
+          i2pandroid:showAsAction="never" >
+    </item>
+    <item android:title="@string/menu_help"
+          android:id="@+id/menu_help"
+          i2pandroid:showAsAction="never" >
+    </item>
+</menu>
diff --git a/res/menu/fragment_addressbook_actions.xml b/res/menu/fragment_addressbook_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..db7b667638081159ba4a114744131c10cb618179
--- /dev/null
+++ b/res/menu/fragment_addressbook_actions.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:i2pandroid="http://schemas.android.com/apk/res-auto" >
+    <!-- Add, should appear as action buttons -->
+    <item android:id="@+id/action_add_to_addressbook"
+          android:title="@string/action_add"
+          android:icon="@drawable/ic_content_new"
+          i2pandroid:showAsAction="ifRoom" />
+    <!-- Settings, Help, should always be in the overflow -->
+    <item android:id="@+id/action_addressbook_settings"
+          android:title="@string/menu_settings"
+          i2pandroid:showAsAction="never" />
+    <item android:id="@+id/action_addressbook_help"
+          android:title="@string/menu_help"
+          i2pandroid:showAsAction="never" />
+</menu>
diff --git a/res/menu/fragment_i2ptunnel_detail_actions.xml b/res/menu/fragment_i2ptunnel_detail_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3c7e47bfdeb837af8448b6566b21b15d4ddca909
--- /dev/null
+++ b/res/menu/fragment_i2ptunnel_detail_actions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:i2pandroid="http://schemas.android.com/apk/res-auto" >
+    <item android:id="@+id/action_start_tunnel"
+          android:title="@string/action_i2ptunnel_start"
+          android:icon="@drawable/ic_av_play"
+          i2pandroid:showAsAction="ifRoom" />
+    <item android:id="@+id/action_stop_tunnel"
+          android:title="@string/action_i2ptunnel_stop"
+          android:icon="@drawable/ic_av_stop"
+          i2pandroid:showAsAction="ifRoom" />
+    <item android:id="@+id/action_edit_tunnel"
+          android:title="@string/action_edit"
+          android:icon="@drawable/ic_content_edit"
+          i2pandroid:showAsAction="ifRoom" />
+    <item android:id="@+id/action_delete_tunnel"
+          android:title="@string/action_delete"
+          android:icon="@drawable/ic_content_discard"
+          i2pandroid:showAsAction="ifRoom" />
+
+</menu>
diff --git a/res/menu/fragment_i2ptunnel_list_actions.xml b/res/menu/fragment_i2ptunnel_list_actions.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c82477bc789584812603acd10eeba10c451dc396
--- /dev/null
+++ b/res/menu/fragment_i2ptunnel_list_actions.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:i2pandroid="http://schemas.android.com/apk/res-auto" >
+    <item android:id="@+id/action_add_tunnel"
+          android:title="@string/action_add"
+          android:icon="@drawable/ic_content_new"
+          i2pandroid:showAsAction="ifRoom" />
+    <item android:id="@+id/action_start_all_tunnels"
+       	  android:title="@string/action_i2ptunnel_start_all"
+       	  i2pandroid:showAsAction="never" />
+    <item android:id="@+id/action_stop_all_tunnels"
+       	  android:title="@string/action_i2ptunnel_stop_all"
+       	  i2pandroid:showAsAction="never" />
+    <item android:id="@+id/action_restart_all_tunnels"
+       	  android:title="@string/action_i2ptunnel_restart_all"
+       	  i2pandroid:showAsAction="never" />
+    <item android:id="@+id/action_i2ptunnel_help"
+          android:title="@string/menu_help"
+          i2pandroid:showAsAction="never" />
+
+</menu>
diff --git a/res/menu/menu1.xml b/res/menu/menu1.xml
deleted file mode 100755
index df87e8276e8a57302df54e5117dd28ad4f7d0210..0000000000000000000000000000000000000000
--- a/res/menu/menu1.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:title="@string/action_router_start"
-          android:id="@+id/menu_start"
-          android:icon="@drawable/ic_menu_play_clip" >
-    </item>
-    <item android:title="@string/action_router_stop"
-          android:id="@+id/menu_stop"
-          android:icon="@android:drawable/ic_menu_close_clear_cancel" >
-    </item>
-    <item android:title="@string/label_addressbook"
-          android:id="@+id/menu_addressbook"
-          android:icon="@drawable/ic_menu_friendslist" >
-    </item>
-    <item android:title="@string/action_reload"
-          android:id="@+id/menu_reload"
-          android:icon="@drawable/ic_menu_refresh" >
-    </item>
-    <item android:title="@string/label_home"
-          android:id="@+id/menu_home"
-          android:icon="@drawable/ic_menu_home" >
-    </item>
-    <item android:title="@string/menu_settings"
-          android:id="@+id/menu_settings"
-          android:icon="@android:drawable/ic_menu_preferences" >
-    </item>
-</menu>
diff --git a/res/values-large/refs.xml b/res/values-large/refs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9de743d3ba14083d212117babcea03a211302f61
--- /dev/null
+++ b/res/values-large/refs.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="activity_navdrawer" type="layout">@layout/activity_navdrawer_twopane</item>
+</resources>
diff --git a/res/values-sw600dp/refs.xml b/res/values-sw600dp/refs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9de743d3ba14083d212117babcea03a211302f61
--- /dev/null
+++ b/res/values-sw600dp/refs.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="activity_navdrawer" type="layout">@layout/activity_navdrawer_twopane</item>
+</resources>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
index ed6cc84d1896bb97d1fed34932dbe99f2f09e29f..44d1430d672984310268dba2efd2ef5fe1f448b2 100644
--- a/res/values/arrays.xml
+++ b/res/values/arrays.xml
@@ -1,5 +1,13 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <resources>
+    <string-array name="navdrawer_activity_titles">
+        <item>@string/label_home</item>
+        <item>@string/label_addressbook</item>
+        <item>@string/label_i2ptunnel</item>
+        <item>@string/label_logs</item>
+        <item>@string/label_error_logs</item>
+        <item>@string/label_peers_status</item>
+    </string-array>
     <string-array name="setting0to3">
         <item>0</item>
         <item>1</item>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bf983966b44b4e1b36069888aa73ec54a471a1f0
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,8 @@
+<resources>
+    <color name="step_pager_previous_tab_color">#4433b5e5</color>
+    <color name="step_pager_selected_tab_color">#ff0099cc</color>
+    <color name="step_pager_selected_last_tab_color">#ff669900</color>
+    <color name="step_pager_next_tab_color">#10000000</color>
+
+    <color name="review_green">#ff669900</color>
+</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b3bf632b3339cfd866ceaec4e24bc08b8c0aa515
--- /dev/null
+++ b/res/values/dimens.xml
@@ -0,0 +1,5 @@
+<resources>
+    <dimen name="step_pager_tab_width">32dp</dimen>
+    <dimen name="step_pager_tab_height">3dp</dimen>
+    <dimen name="step_pager_tab_spacing">4dp</dimen>
+</resources>
diff --git a/res/values/refs.xml b/res/values/refs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8c0aba38c154cdc6d7cc193cd54bc3ed5918f3e7
--- /dev/null
+++ b/res/values/refs.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <item name="activity_navdrawer" type="layout">@layout/activity_navdrawer_onepane</item>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 280690a2fb9acff8af07656df2533e794bc0a02a..76ce6b5f7f8aea7d216f12365216c823ce083549 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2,6 +2,7 @@
 <resources>
 
     <string name="app_name">I2P</string>
+    <string name="desc_i2p_logo">I2P logo</string>
     <string name="welcome_new_install">Welcome to I2P! This app is ALPHA software and it does not provide strong anonymity. Please read the release notes and license information.</string>
     <string name="welcome_new_version">New version installed. Please read the release notes. Version:</string>
 
@@ -10,6 +11,9 @@
     <string name="label_main_controls">Main Controls</string>
     <string name="label_tools">I2P Tools</string>
     <string name="label_addressbook">Addressbook</string>
+    <string name="label_i2ptunnel">I2PTunnel</string>
+    <string name="label_i2ptunnel_client">Client tunnels</string>
+    <string name="label_i2ptunnel_server">Server tunnels</string>
     <string name="label_logs">Logs</string>
     <string name="label_error_logs">Error Logs</string>
     <string name="label_info_and_pages">Information and pages</string>
@@ -22,11 +26,23 @@
     <string name="label_nonanon_info">Non Anonymous Information</string>
     <string name="label_website_nonanon">Web Site (non-anon)</string>
     <string name="label_faq_nonanon">FAQ (non-anon)</string>
+    <string name="label_browse">Browse</string>
 
+    <string name="drawer_open">Open nav</string>
+    <string name="drawer_close">Close nav</string>
+    <string name="action_search">Search</string>
     <string name="action_add">Add</string>
+    <string name="action_edit">Edit</string>
+    <string name="action_delete">Delete</string>
     <string name="action_router_start">Start Router</string>
     <string name="action_router_stop">Stop Router</string>
+    <string name="action_i2ptunnel_start">Start tunnel</string>
+    <string name="action_i2ptunnel_stop">Stop tunnel</string>
+    <string name="action_i2ptunnel_start_all">Start all tunnels</string>
+    <string name="action_i2ptunnel_stop_all">Stop all tunnels</string>
+    <string name="action_i2ptunnel_restart_all">Restart all tunnels</string>
     <string name="action_reload">Reload</string>
+    <string name="hint_search_addressbook">Search addressbook</string>
 
     <string name="menu_settings">Settings</string>
     <string name="settings_enable">Enable</string>
@@ -64,4 +80,59 @@
     <string name="settings_label_expl_backupQuantity">Backup quantity</string>
     <string name="settings_desc_expl_backupQuantity">How many tunnel backups</string>
 
+    <string name="menu_help">Help</string>
+
+    <string name="i2ptunnel_type_client">Standard client</string>
+    <string name="i2ptunnel_type_httpclient">HTTP client</string>
+    <string name="i2ptunnel_type_ircclient">IRC client</string>
+    <string name="i2ptunnel_type_server">Standard server</string>
+    <string name="i2ptunnel_type_httpserver">HTTP server</string>
+    <string name="i2ptunnel_type_sockstunnel">SOCKS 4/4a/5 proxy</string>
+    <string name="i2ptunnel_type_socksirctunnel">SOCKS IRC proxy</string>
+    <string name="i2ptunnel_type_connectclient">CONNECT/SSL/HTTPS proxy</string>
+    <string name="i2ptunnel_type_ircserver">IRC server</string>
+    <string name="i2ptunnel_type_streamrclient">Streamr client</string>
+    <string name="i2ptunnel_type_streamrserver">Streamr server</string>
+    <string name="i2ptunnel_type_httpbidirserver">HTTP bidir</string>
+
+    <string name="i2ptunnel_not_initialized">Tunnels are not initialized yet, please reload in two minutes.</string>
+    <string name="i2ptunnel_new_tunnel">New Tunnel</string>
+    <string name="i2ptunnel_msg_config_saved">Configuration changes saved</string>
+    <string name="i2ptunnel_msg_config_save_failed">Failed to save configuration</string>
+    <string name="i2ptunnel_msg_tunnel_starting">Starting tunnel</string>
+    <string name="i2ptunnel_msg_tunnel_stopping">Stopping tunnel</string>
+    <string name="i2ptunnel_delete_confirm_message">Delete tunnel?</string>
+    <string name="i2ptunnel_delete_confirm_button">Delete tunnel</string>
+
+    <string name="i2ptunnel_wizard_k_client_server">Client or Server</string>
+    <string name="i2ptunnel_wizard_v_client">Client tunnel</string>
+    <string name="i2ptunnel_wizard_v_server">Server tunnel</string>
+    <string name="i2ptunnel_wizard_k_type">Tunnel type</string>
+    <string name="i2ptunnel_wizard_k_name">Name</string>
+    <string name="i2ptunnel_wizard_k_desc">Description</string>
+    <string name="i2ptunnel_wizard_k_dest">Destination</string>
+    <string name="i2ptunnel_wizard_k_outproxies">Outproxies</string>
+    <string name="i2ptunnel_wizard_k_target_host">Target host</string>
+    <string name="i2ptunnel_wizard_k_target_port">Target port</string>
+    <string name="i2ptunnel_wizard_k_reachable_on">Reachable on</string>
+    <string name="i2ptunnel_wizard_k_binding_port">Binding port</string>
+    <string name="i2ptunnel_wizard_k_auto_start">Auto start</string>
+
+    <string name="next">Next</string>
+    <string name="prev">Previous</string>
+    <string name="finish">Submit</string>
+    <string name="review">Review</string>
+    <string name="enabled">Enabled</string>
+
+    <string name="i2ptunnel_wizard_desc_name">The name of the tunnel, for identification in the tunnel list.</string>
+    <string name="i2ptunnel_wizard_desc_desc">A description of the tunnel. This is optional and purely informative.</string>
+    <string name="i2ptunnel_wizard_desc_dest">Type in the I2P destination of the service that this client tunnel should connect to. This could be the full base 64 destination key, or an I2P URL from your address book.</string>
+    <string name="i2ptunnel_wizard_desc_outproxies">If you know of any outproxies for this type of tunnel (either HTTP or SOCKS), fill them in. Separate multiple proxies with commas.</string>
+    <string name="i2ptunnel_wizard_desc_target_host">This is the IP that your service is running on, this is usually on the same machine so 127.0.0.1 is autofilled.</string>
+    <string name="i2ptunnel_wizard_desc_target_port">This is the port that the service is accepting connections on.</string>
+    <string name="i2ptunnel_wizard_desc_reachable_on">This limits what computers or smartphones can access this tunnel.</string>
+    <string name="i2ptunnel_wizard_desc_binding_port">This is the port that the client tunnel will be accessed from locally. This is also the client port for the HTTP bidir server tunnel.</string>
+    <string name="i2ptunnel_wizard_desc_auto_start">Should the tunnel automatically start when the router starts?</string>
+    <string name="i2ptunnel_wizard_submit_confirm_message">Create tunnel?</string>
+    <string name="i2ptunnel_wizard_submit_confirm_button">Create tunnel</string>
 </resources>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b35b21f8f7945b0cf507363cf9044c271af6d9ff
--- /dev/null
+++ b/res/values/styles.xml
@@ -0,0 +1,29 @@
+<resources>
+    <style name="WizardPageContainer">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">match_parent</item>
+        <item name="android:orientation">vertical</item>
+    </style>
+
+    <style name="WizardPageTitle">
+        <item name="android:id">@android:id/title</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginBottom">8dp</item>
+        <item name="android:layout_marginLeft">16dp</item>
+        <item name="android:layout_marginRight">16dp</item>
+        <item name="android:paddingLeft">?android:attr/listPreferredItemPaddingLeft</item>
+        <item name="android:textSize">36sp</item>
+        <item name="android:textColor">#ff0099cc</item>
+    </style>
+
+    <style name="WizardFormLabel">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginBottom">0dp</item>
+        <item name="android:textAppearance">?android:textAppearanceSmall</item>
+        <item name="android:textStyle">bold</item>
+        <item name="android:paddingLeft">12dp</item>
+        <item name="android:paddingRight">12dp</item>
+    </style>
+</resources>
diff --git a/res/xml/searchable_addressbook.xml b/res/xml/searchable_addressbook.xml
new file mode 100644
index 0000000000000000000000000000000000000000..50dc9fb3208dbf367e105f9a7be351727253aac8
--- /dev/null
+++ b/res/xml/searchable_addressbook.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+    android:label="@string/label_addressbook"
+    android:hint="@string/hint_search_addressbook" >
+</searchable>
diff --git a/routerjars/AndroidManifest.xml.in b/routerjars/AndroidManifest.xml.in
index 4f76fdedfe033e5308bd60a625ef9817225cd5d9..cd7a189d17102d0c93073512569aea0e177c52a0 100644
--- a/routerjars/AndroidManifest.xml.in
+++ b/routerjars/AndroidManifest.xml.in
@@ -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-0_b0-API8"
+      android:versionCode="0"
+      android:versionName="0.0.0-0_b0-API8"
       android:installLocation="auto"
       >
 
diff --git a/scripts/setversion.sh b/scripts/setversion.sh
index c55005994fae634e12f1bd9bfa24e10327c6750c..6fb89954898776d60e80c907f9710bbebf5b10e0 100755
--- a/scripts/setversion.sh
+++ b/scripts/setversion.sh
@@ -66,12 +66,12 @@ echo "Android version: '$VERSIONSTRING' (${VERSIONINT})"
 echo "my.version.name=${VERSIONSTRING}" > version.properties
 echo "my.version.code=${VERSIONINT}" >> version.properties
 
-SUBST='s/android.versionCode="[0-9]*"/android.versionCode="'${VERSIONINT}'"/'
+SUBST='s/android:versionCode="[0-9]*"/android:versionCode="'${VERSIONINT}'"/'
 sed "$SUBST" < $MANIFEST > $TMP
-SUBST='s/android.versionName="[^"]*"/android.versionName="'${VERSIONSTRING}'"/'
+SUBST='s/android:versionName="[^"]*"/android:versionName="'${VERSIONSTRING}'"/'
 sed "$SUBST" < $TMP > $MANIFEST
-SUBST='s/android.versionCode="[0-9]*"/android.versionCode="'${VERSIONINT}'"/'
+SUBST='s/android:versionCode="[0-9]*"/android:versionCode="'${VERSIONINT}'"/'
 sed "$SUBST" < $MANIFESTROUTER > $TMP
-SUBST='s/android.versionName="[^"]*"/android.versionName="'${VERSIONSTRING}'"/'
+SUBST='s/android:versionName="[^"]*"/android:versionName="'${VERSIONSTRING}'"/'
 sed "$SUBST" < $TMP > $MANIFESTROUTER
 rm -f $TMP
diff --git a/src/com/hlidskialf/android/preference/SeekBarPreference.java b/src/com/hlidskialf/android/preference/SeekBarPreference.java
index 707b516595102c163e85130fcf2dd5c6e07bcd89..723d0d5d0a19067cd6ef975879f5637660d3c776 100644
--- a/src/com/hlidskialf/android/preference/SeekBarPreference.java
+++ b/src/com/hlidskialf/android/preference/SeekBarPreference.java
@@ -49,8 +49,14 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
     public SeekBarPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
-        mDialogMessage = attrs.getAttributeValue(androidns, "dialogMessage");
-        mSuffix = attrs.getAttributeValue(androidns, "text");
+        int dialogMessageR = attrs.getAttributeResourceValue(androidns, "dialogMessage", 0);
+        mDialogMessage = (dialogMessageR == 0)
+                ? attrs.getAttributeValue(androidns, "dialogMessage")
+                : context.getResources().getString(dialogMessageR);
+        int textR = attrs.getAttributeResourceValue(androidns, "text", 0);
+        mSuffix = (textR == 0)
+                ? attrs.getAttributeValue(androidns, "text")
+                : context.getResources().getString(textR);
         mDefault = attrs.getAttributeValue(androidns, "defaultValue");
         mMax = Integer.parseInt(attrs.getAttributeValue(androidns, "max"));
         if (attrs.getAttributeValue(androidns, "direction") != null) {
@@ -84,7 +90,7 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
         mValueText.setGravity(Gravity.CENTER_HORIZONTAL);
         mValueText.setTextSize(32);
         params = new LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.FILL_PARENT,
+                LinearLayout.LayoutParams.MATCH_PARENT,
                 LinearLayout.LayoutParams.WRAP_CONTENT);
         layout.addView(mValueText, params);
 
@@ -93,7 +99,7 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
         // Move the bar away from the changing text, so you can see it, and
         // move it away from the edges to improve usability for the end-ranges.
         mSeekBar.setPadding(6, 30, 6, 6);
-        layout.addView(mSeekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
+        layout.addView(mSeekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
 
         if (shouldPersist()) {
             mValue = Integer.parseInt(getPersistedString(mDefault));
@@ -126,7 +132,7 @@ public class SeekBarPreference extends DialogPreference implements SeekBar.OnSee
         if (shouldPersist()) {
             persistString(t);
         }
-        callChangeListener(new Integer(value));
+        callChangeListener(Integer.valueOf(value));
     }
 
     public void onStartTrackingTouch(SeekBar seek) {
diff --git a/src/net/i2p/android/i2ptunnel/activity/TunnelDetailActivity.java b/src/net/i2p/android/i2ptunnel/activity/TunnelDetailActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..8965506a19275e9af8781b1d8b91ee2647b26f96
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/activity/TunnelDetailActivity.java
@@ -0,0 +1,28 @@
+package net.i2p.android.i2ptunnel.activity;
+
+import net.i2p.android.i2ptunnel.fragment.TunnelDetailFragment;
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.I2PActivityBase;
+import android.os.Bundle;
+
+public class TunnelDetailActivity extends I2PActivityBase implements
+        TunnelDetailFragment.OnTunnelDeletedListener {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mDrawerToggle.setDrawerIndicatorEnabled(false);
+
+        if (savedInstanceState == null) {
+            int tunnelId = getIntent().getIntExtra(TunnelDetailFragment.TUNNEL_ID, 0);
+            TunnelDetailFragment detailFrag = TunnelDetailFragment.newInstance(tunnelId);
+            getSupportFragmentManager().beginTransaction()
+                .add(R.id.main_fragment, detailFrag).commit();
+        }
+    }
+
+    // TunnelDetailFragment.OnTunnelDeletedListener
+
+    public void onTunnelDeleted(int tunnelId, int numTunnelsLeft) {
+        finish();
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/activity/TunnelListActivity.java b/src/net/i2p/android/i2ptunnel/activity/TunnelListActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..c8d318daa57993116e1e0dca130d28df5996ee74
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/activity/TunnelListActivity.java
@@ -0,0 +1,104 @@
+package net.i2p.android.i2ptunnel.activity;
+
+import net.i2p.android.i2ptunnel.fragment.TunnelDetailFragment;
+import net.i2p.android.i2ptunnel.fragment.TunnelListFragment;
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.I2PActivityBase;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBar.Tab;
+
+public class TunnelListActivity extends I2PActivityBase implements
+        TunnelListFragment.OnTunnelSelectedListener,
+        TunnelDetailFragment.OnTunnelDeletedListener {
+    /**
+     * Whether or not the activity is in two-pane mode, i.e. running on a tablet
+     * device.
+     */
+    private boolean mTwoPane;
+
+    @Override
+    protected boolean canUseTwoPanes() {
+        return true;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Set up action bar for tabs
+        ActionBar actionBar = getSupportActionBar();
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+        // Client tunnels tab
+        TunnelListFragment cf = new TunnelListFragment();
+        Bundle args = new Bundle();
+        args.putBoolean(TunnelListFragment.SHOW_CLIENT_TUNNELS, true);
+        cf.setArguments(args);
+        Tab tab = actionBar.newTab()
+                .setText(R.string.label_i2ptunnel_client)
+                .setTabListener(new TabListener(cf));
+        actionBar.addTab(tab);
+
+        // Server tunnels tab
+        TunnelListFragment sf = new TunnelListFragment();
+        args = new Bundle();
+        args.putBoolean(TunnelListFragment.SHOW_CLIENT_TUNNELS, false);
+        sf.setArguments(args);
+        tab = actionBar.newTab()
+                .setText(R.string.label_i2ptunnel_server)
+                .setTabListener(new TabListener(sf));
+        actionBar.addTab(tab);
+
+        if (findViewById(R.id.detail_fragment) != null) {
+            // The detail container view will be present only in the
+            // large-screen layouts (res/values-large and
+            // res/values-sw600dp). If this view is present, then the
+            // activity should be in two-pane mode.
+            mTwoPane = true;
+
+            // In two-pane mode, list items should be given the
+            // 'activated' state when touched.
+            cf.setActivateOnItemClick(true);
+            sf.setActivateOnItemClick(true);
+        }
+    }
+
+    // TunnelListFragment.OnTunnelSelectedListener
+
+    public void onTunnelSelected(int tunnelId) {
+        if (mTwoPane) {
+            // In two-pane mode, show the detail view in this activity by
+            // adding or replacing the detail fragment using a
+            // fragment transaction.
+            TunnelDetailFragment detailFrag = TunnelDetailFragment.newInstance(tunnelId);
+            getSupportFragmentManager().beginTransaction()
+                .replace(R.id.detail_fragment, detailFrag).commit();
+        } else {
+            // In single-pane mode, simply start the detail activity
+            // for the selected item ID.
+            Intent detailIntent = new Intent(this, TunnelDetailActivity.class);
+            detailIntent.putExtra(TunnelDetailFragment.TUNNEL_ID, tunnelId);
+            startActivity(detailIntent);
+        }
+    }
+
+    // TunnelDetailFragment.OnTunnelDeletedListener
+
+    public void onTunnelDeleted(int tunnelId, int numTunnelsLeft) {
+        // Should only get here in two-pane mode, but just to be safe:
+        if (mTwoPane) {
+            if (numTunnelsLeft > 0) {
+                TunnelDetailFragment detailFrag = TunnelDetailFragment.newInstance(
+                        (tunnelId > 0 ? tunnelId - 1 : 0));
+                getSupportFragmentManager().beginTransaction()
+                    .replace(R.id.detail_fragment, detailFrag).commit();
+            } else {
+                TunnelDetailFragment detailFrag = (TunnelDetailFragment) getSupportFragmentManager().findFragmentById(R.id.detail_fragment);
+                getSupportFragmentManager().beginTransaction()
+                    .remove(detailFrag).commit();
+            }
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a977ebcb4c5fa1fd87bf478e1a73941f06cfb0f
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java
@@ -0,0 +1,272 @@
+package net.i2p.android.i2ptunnel.activity;
+
+import java.util.List;
+
+import net.i2p.android.i2ptunnel.fragment.TunnelListFragment;
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.AbstractWizardModel;
+import net.i2p.android.wizard.model.ModelCallbacks;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.ui.PageFragmentCallbacks;
+import net.i2p.android.wizard.ui.ReviewFragment;
+import net.i2p.android.wizard.ui.StepPagerStrip;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+public class TunnelWizardActivity extends FragmentActivity implements
+        PageFragmentCallbacks,
+        ReviewFragment.Callbacks,
+        ModelCallbacks {
+    private ViewPager mPager;
+    private MyPagerAdapter mPagerAdapter;
+
+    private boolean mEditingAfterReview;
+
+    private AbstractWizardModel mWizardModel;
+
+    private boolean mConsumePageSelectedEvent;
+
+    private Button mNextButton;
+    private Button mPrevButton;
+
+    private List<Page> mCurrentPageSequence;
+    private StepPagerStrip mStepPagerStrip;
+
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_wizard);
+
+        mWizardModel = new TunnelWizardModel(this);
+        if (savedInstanceState != null)
+            mWizardModel.load(savedInstanceState.getBundle("model"));
+
+        mWizardModel.registerListener(this);
+
+        mPagerAdapter = new MyPagerAdapter(getSupportFragmentManager());
+        mPager = (ViewPager) findViewById(R.id.pager);
+        mPager.setAdapter(mPagerAdapter);
+        mStepPagerStrip = (StepPagerStrip) findViewById(R.id.strip);
+        mStepPagerStrip.setOnPageSelectedListener(new StepPagerStrip.OnPageSelectedListener() {
+            public void onPageStripSelected(int position) {
+                position = Math.min(mPagerAdapter.getCount() - 1, position);
+                if (mPager.getCurrentItem() != position) {
+                    mPager.setCurrentItem(position);
+                }
+            }
+        });
+
+        mNextButton = (Button) findViewById(R.id.next_button);
+        mPrevButton = (Button) findViewById(R.id.prev_button);
+
+        mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+            @Override
+            public void onPageSelected(int position) {
+                mStepPagerStrip.setCurrentPage(position);
+
+                if (mConsumePageSelectedEvent) {
+                    mConsumePageSelectedEvent = false;
+                    return;
+                }
+
+                mEditingAfterReview = false;
+                updateBottomBar();
+            }
+        });
+
+        mNextButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                if (mPager.getCurrentItem() == mCurrentPageSequence.size()) {
+                    DialogFragment dg = new DialogFragment() {
+                        @Override
+                        public Dialog onCreateDialog(Bundle savedInstanceState) {
+                            return new AlertDialog.Builder(getActivity())
+                                    .setMessage(R.string.i2ptunnel_wizard_submit_confirm_message)
+                                    .setPositiveButton(R.string.i2ptunnel_wizard_submit_confirm_button,
+                                            new DialogInterface.OnClickListener() {
+
+                                                public void onClick(DialogInterface dialog, int which) {
+                                                    Intent result = new Intent();
+                                                    result.putExtra(TunnelListFragment.TUNNEL_WIZARD_DATA, mWizardModel.save());
+                                                    setResult(Activity.RESULT_OK, result);
+                                                    dialog.dismiss();
+                                                    finish();
+                                                }
+                                            })
+                                    .setNegativeButton(android.R.string.cancel, null)
+                                    .create();
+                        }
+                    };
+                    dg.show(getSupportFragmentManager(), "create_tunnel_dialog");
+                } else {
+                    if (mEditingAfterReview) {
+                        mPager.setCurrentItem(mPagerAdapter.getCount() - 1);
+                    } else {
+                        mPager.setCurrentItem(mPager.getCurrentItem() + 1);
+                    }
+                }
+            }
+        });
+
+        mPrevButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                mPager.setCurrentItem(mPager.getCurrentItem() - 1);
+            }
+        });
+
+        onPageTreeChanged();
+        updateBottomBar();
+    }
+
+    public void onPageTreeChanged() {
+        mCurrentPageSequence = mWizardModel.getCurrentPageSequence();
+        recalculateCutOffPage();
+        mStepPagerStrip.setPageCount(mCurrentPageSequence.size() + 1); // + 1 = review step
+        mPagerAdapter.notifyDataSetChanged();
+        updateBottomBar();
+    }
+
+    private void updateBottomBar() {
+        int position = mPager.getCurrentItem();
+        if (position == mCurrentPageSequence.size()) {
+            mNextButton.setText(R.string.finish);
+        } else {
+            mNextButton.setText(mEditingAfterReview
+                    ? R.string.review
+                    : R.string.next);
+            TypedValue v = new TypedValue();
+            getTheme().resolveAttribute(android.R.attr.textAppearanceMedium, v, true);
+            mNextButton.setTextAppearance(this, v.resourceId);
+            mNextButton.setEnabled(position != mPagerAdapter.getCutOffPage());
+        }
+
+        mPrevButton.setVisibility(position <= 0 ? View.INVISIBLE : View.VISIBLE);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mWizardModel.unregisterListener(this);
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBundle("model", mWizardModel.save());
+    }
+
+    public AbstractWizardModel onGetModel() {
+        return mWizardModel;
+    }
+
+    public void onEditScreenAfterReview(String key) {
+        for (int i = mCurrentPageSequence.size() - 1; i >= 0; i--) {
+            if (mCurrentPageSequence.get(i).getKey().equals(key)) {
+                mConsumePageSelectedEvent = true;
+                mEditingAfterReview = true;
+                mPager.setCurrentItem(i);
+                updateBottomBar();
+                break;
+            }
+        }
+    }
+
+    public void onPageDataChanged(Page page) {
+        if (page.isRequired()) {
+            if (recalculateCutOffPage()) {
+                mPagerAdapter.notifyDataSetChanged();
+                updateBottomBar();
+            }
+        }
+    }
+
+    public Page onGetPage(String key) {
+        return mWizardModel.findByKey(key);
+    }
+
+    private boolean recalculateCutOffPage() {
+        // Cut off the pager adapter at first required page that isn't completed
+        int cutOffPage = mCurrentPageSequence.size() + 1;
+        for (int i = 0; i < mCurrentPageSequence.size(); i++) {
+            Page page = mCurrentPageSequence.get(i);
+            if (page.isRequired() && !page.isCompleted()) {
+                cutOffPage = i;
+                break;
+            }
+        }
+
+        if (mPagerAdapter.getCutOffPage() != cutOffPage) {
+            mPagerAdapter.setCutOffPage(cutOffPage);
+            return true;
+        }
+
+        return false;
+    }
+
+    public class MyPagerAdapter extends FragmentStatePagerAdapter {
+        private int mCutOffPage;
+        private Fragment mPrimaryItem;
+
+        public MyPagerAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        @Override
+        public Fragment getItem(int i) {
+            if (i >= mCurrentPageSequence.size()) {
+                return new ReviewFragment();
+            }
+
+            return mCurrentPageSequence.get(i).createFragment();
+        }
+
+        @Override
+        public int getItemPosition(Object object) {
+            // TODO: be smarter about this
+            if (object == mPrimaryItem) {
+                // Re-use the current fragment (its position never changes)
+                return POSITION_UNCHANGED;
+            }
+
+            return POSITION_NONE;
+        }
+
+        @Override
+        public void setPrimaryItem(ViewGroup container, int position, Object object) {
+            super.setPrimaryItem(container, position, object);
+            mPrimaryItem = (Fragment) object;
+        }
+
+        @Override
+        public int getCount() {
+            if (mCurrentPageSequence == null) {
+                return 0;
+            }
+            return Math.min(mCutOffPage + 1, mCurrentPageSequence.size() + 1);
+        }
+
+        public void setCutOffPage(int cutOffPage) {
+            if (cutOffPage < 0) {
+                cutOffPage = Integer.MAX_VALUE;
+            }
+            mCutOffPage = cutOffPage;
+        }
+
+        public int getCutOffPage() {
+            return mCutOffPage;
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/activity/TunnelWizardModel.java b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..27abeaf4ac5d11146287845408126e5e20b45023
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardModel.java
@@ -0,0 +1,123 @@
+package net.i2p.android.i2ptunnel.activity;
+
+import android.content.Context;
+import android.content.res.Resources;
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.AbstractWizardModel;
+import net.i2p.android.wizard.model.BranchPage;
+import net.i2p.android.wizard.model.Conditional;
+import net.i2p.android.wizard.model.I2PDestinationPage;
+import net.i2p.android.wizard.model.PageList;
+import net.i2p.android.wizard.model.SingleFixedBooleanPage;
+import net.i2p.android.wizard.model.SingleFixedChoicePage;
+import net.i2p.android.wizard.model.SingleTextFieldPage;
+
+public class TunnelWizardModel extends AbstractWizardModel {
+    public TunnelWizardModel(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected PageList onNewRootPageList() {
+        Resources res = mContext.getResources();
+        Conditional cTunnelType = new Conditional();
+        Conditional cClientType = new Conditional();
+        Conditional cServerType = new Conditional();
+
+        return new PageList(
+            new BranchPage(this, res.getString(R.string.i2ptunnel_wizard_k_client_server))
+                .addBranch(res.getString(R.string.i2ptunnel_wizard_v_client),
+                    new SingleFixedChoicePage(this, res.getString(R.string.i2ptunnel_wizard_k_type))
+                        .setChoices(
+                                res.getString(R.string.i2ptunnel_type_client),
+                                res.getString(R.string.i2ptunnel_type_httpclient),
+                                res.getString(R.string.i2ptunnel_type_ircclient),
+                                res.getString(R.string.i2ptunnel_type_sockstunnel),
+                                res.getString(R.string.i2ptunnel_type_socksirctunnel),
+                                res.getString(R.string.i2ptunnel_type_connectclient),
+                                res.getString(R.string.i2ptunnel_type_streamrclient))
+                        .setRequired(true)
+                        .makeConditional(cClientType))
+                .addBranch(res.getString(R.string.i2ptunnel_wizard_v_server),
+                    new SingleFixedChoicePage(this, res.getString(R.string.i2ptunnel_wizard_k_type))
+                        .setChoices(
+                                res.getString(R.string.i2ptunnel_type_server),
+                                res.getString(R.string.i2ptunnel_type_httpserver),
+                                res.getString(R.string.i2ptunnel_type_httpbidirserver),
+                                res.getString(R.string.i2ptunnel_type_ircserver),
+                                res.getString(R.string.i2ptunnel_type_streamrserver))
+                        .setRequired(true)
+                        .makeConditional(cServerType))
+                .setRequired(true)
+                .makeConditional(cTunnelType),
+
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_name))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_name))
+                .setRequired(true),
+
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_desc))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_desc)),
+
+            new I2PDestinationPage(this, res.getString(R.string.i2ptunnel_wizard_k_dest))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_dest))
+                .setRequired(true)
+                .setEqualAnyCondition(cClientType,
+                        res.getString(R.string.i2ptunnel_type_client),
+                        res.getString(R.string.i2ptunnel_type_ircclient),
+                        res.getString(R.string.i2ptunnel_type_streamrclient)),
+
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_outproxies))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_outproxies))
+                .setEqualAnyCondition(cClientType,
+                        res.getString(R.string.i2ptunnel_type_httpclient),
+                        res.getString(R.string.i2ptunnel_type_connectclient),
+                        res.getString(R.string.i2ptunnel_type_sockstunnel),
+                        res.getString(R.string.i2ptunnel_type_socksirctunnel)),
+
+            // Not set required because a default is specified.
+            // Otherwise user would need to edit the field to
+            // enable the Next button.
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_target_host))
+                .setDefault("127.0.0.1")
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_target_host))
+                .setEqualCondition(cClientType,
+                        res.getString(R.string.i2ptunnel_type_streamrclient))
+                .setEqualAnyCondition(cServerType,
+                        res.getString(R.string.i2ptunnel_type_server),
+                        res.getString(R.string.i2ptunnel_type_httpserver),
+                        res.getString(R.string.i2ptunnel_type_httpbidirserver),
+                        res.getString(R.string.i2ptunnel_type_ircserver)),
+
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_target_port))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_target_port))
+                .setRequired(true)
+                .setEqualCondition(cTunnelType, res.getString(R.string.i2ptunnel_wizard_v_server)),
+
+            // Not set required because a default is specified.
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_reachable_on))
+                .setDefault("127.0.0.1")
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_reachable_on))
+                .setEqualAnyCondition(cClientType,
+                        res.getString(R.string.i2ptunnel_type_client),
+                        res.getString(R.string.i2ptunnel_type_httpclient),
+                        res.getString(R.string.i2ptunnel_type_ircclient),
+                        res.getString(R.string.i2ptunnel_type_sockstunnel),
+                        res.getString(R.string.i2ptunnel_type_socksirctunnel),
+                        res.getString(R.string.i2ptunnel_type_connectclient))
+                .setEqualAnyCondition(cServerType,
+                        res.getString(R.string.i2ptunnel_type_httpbidirserver),
+                        res.getString(R.string.i2ptunnel_type_streamrserver)),
+
+            new SingleTextFieldPage(this, res.getString(R.string.i2ptunnel_wizard_k_binding_port))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_k_binding_port))
+                .setRequired(true)
+                .setEqualCondition(cTunnelType, res.getString(R.string.i2ptunnel_wizard_v_client))
+                .setEqualCondition(cServerType, res.getString(R.string.i2ptunnel_type_httpbidirserver)),
+
+            new SingleFixedBooleanPage(this, res.getString(R.string.i2ptunnel_wizard_k_auto_start))
+                .setDescription(res.getString(R.string.i2ptunnel_wizard_desc_auto_start))
+                .setRequired(true)
+            );
+    }
+
+}
diff --git a/src/net/i2p/android/i2ptunnel/adapter/TunnelEntryAdapter.java b/src/net/i2p/android/i2ptunnel/adapter/TunnelEntryAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..87bb8df5236d725a09e57ac3a7658743bc39a1f4
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/adapter/TunnelEntryAdapter.java
@@ -0,0 +1,54 @@
+package net.i2p.android.i2ptunnel.adapter;
+
+import java.util.List;
+
+import net.i2p.android.i2ptunnel.loader.TunnelEntry;
+import net.i2p.android.router.R;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class TunnelEntryAdapter extends ArrayAdapter<TunnelEntry> {
+    private final LayoutInflater mInflater;
+
+    public TunnelEntryAdapter(Context context) {
+        super(context, android.R.layout.simple_list_item_2);
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    public void setData(List<TunnelEntry> tunnels) {
+        clear();
+        if (tunnels != null) {
+            for (TunnelEntry tunnel : tunnels) {
+                add(tunnel);
+            }
+        }
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View v = mInflater.inflate(R.layout.listitem_i2ptunnel, parent, false);
+        TunnelEntry tunnel = getItem(position);
+
+        TextView name = (TextView) v.findViewById(R.id.tunnel_name);
+        name.setText(tunnel.getName());
+
+        TextView type = (TextView) v.findViewById(R.id.tunnel_type);
+        type.setText(tunnel.getType());
+
+        TextView ifacePort = (TextView) v.findViewById(R.id.tunnel_interface_port);
+        ifacePort.setText(tunnel.getIfacePort());
+
+        TextView details = (TextView) v.findViewById(R.id.tunnel_details);
+        details.setText(tunnel.getDetails());
+
+        ImageView status = (ImageView) v.findViewById(R.id.tunnel_status);
+        status.setImageDrawable(tunnel.getStatusIcon());
+
+        return v;
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/fragment/TunnelDetailFragment.java b/src/net/i2p/android/i2ptunnel/fragment/TunnelDetailFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..f41c4cda480a7ed65a510077c34dec8f9f76c4a3
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/fragment/TunnelDetailFragment.java
@@ -0,0 +1,178 @@
+package net.i2p.android.i2ptunnel.fragment;
+
+import java.util.List;
+
+import net.i2p.android.i2ptunnel.loader.TunnelEntry;
+import net.i2p.android.i2ptunnel.util.TunnelUtil;
+import net.i2p.android.router.R;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class TunnelDetailFragment extends Fragment {
+    public static final String TUNNEL_ID = "tunnel_id";
+
+    OnTunnelDeletedListener mCallback;
+    private TunnelControllerGroup mGroup;
+    private TunnelEntry mTunnel;
+
+    public static TunnelDetailFragment newInstance(int tunnelId) {
+        TunnelDetailFragment f = new TunnelDetailFragment();
+        Bundle args = new Bundle();
+        args.putInt(TUNNEL_ID, tunnelId);
+        f.setArguments(args);
+        return f;
+    }
+
+    // Container Activity must implement this interface
+    public interface OnTunnelDeletedListener {
+        public void onTunnelDeleted(int tunnelId, int numTunnelsLeft);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // This makes sure that the container activity has implemented
+        // the callback interface. If not, it throws an exception
+        try {
+            mCallback = (OnTunnelDeletedListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement OnTunnelDeletedListener");
+        }
+
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+
+        String error;
+        try {
+            mGroup = TunnelControllerGroup.getInstance();
+            error = mGroup == null ? getResources().getString(R.string.i2ptunnel_not_initialized) : null;
+        } catch (IllegalArgumentException iae) {
+            mGroup = null;
+            error = iae.toString();
+        }
+
+        if (mGroup == null) {
+            // Show error
+        } else if (getArguments().containsKey(TUNNEL_ID)) {
+            int tunnelId = getArguments().getInt(TUNNEL_ID);
+            mTunnel = new TunnelEntry(getActivity(),
+                    mGroup.getControllers().get(tunnelId),
+                    tunnelId);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.fragment_i2ptunnel_detail, container, false);
+
+        if (mTunnel != null) {
+            TextView name = (TextView) v.findViewById(R.id.tunnel_name);
+            name.setText(mTunnel.getName());
+
+            TextView type = (TextView) v.findViewById(R.id.tunnel_type);
+            type.setText(mTunnel.getType());
+
+            TextView description = (TextView) v.findViewById(R.id.tunnel_description);
+            description.setText(mTunnel.getDescription());
+
+            TextView targetIfacePort = (TextView) v.findViewById(R.id.tunnel_target_interface_port);
+            targetIfacePort.setText(mTunnel.getIfacePort());
+
+            TextView accessIfacePort = (TextView) v.findViewById(R.id.tunnel_access_interface_port);
+            accessIfacePort.setText(mTunnel.getIfacePort());
+        }
+
+        return v;
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.fragment_i2ptunnel_detail_actions, menu);
+    }
+
+    @Override
+    public void onPrepareOptionsMenu(Menu menu) {
+        boolean isStopped = mTunnel.getStatus() == TunnelEntry.NOT_RUNNING;
+
+        MenuItem start = menu.findItem(R.id.action_start_tunnel);
+        start.setVisible(isStopped);
+        start.setEnabled(isStopped);
+
+        MenuItem stop = menu.findItem(R.id.action_stop_tunnel);
+        stop.setVisible(!isStopped);
+        stop.setEnabled(!isStopped);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle presses on the action bar items
+        switch (item.getItemId()) {
+        case R.id.action_start_tunnel:
+            mTunnel.getController().startTunnelBackground();
+            Toast.makeText(getActivity().getApplicationContext(),
+                    getResources().getString(R.string.i2ptunnel_msg_tunnel_starting)
+                    + ' ' + mTunnel.getName(), Toast.LENGTH_LONG).show();
+            // Reload the action bar to change the start/stop action
+            getActivity().supportInvalidateOptionsMenu();
+            return true;
+        case R.id.action_stop_tunnel:
+            mTunnel.getController().stopTunnel();
+            Toast.makeText(getActivity().getApplicationContext(),
+                    getResources().getString(R.string.i2ptunnel_msg_tunnel_stopping)
+                    + ' ' + mTunnel.getName(), Toast.LENGTH_LONG).show();
+            // Reload the action bar to change the start/stop action
+            getActivity().supportInvalidateOptionsMenu();
+            return true;
+        case R.id.action_edit_tunnel:
+            return true;
+        case R.id.action_delete_tunnel:
+            DialogFragment dg = new DialogFragment() {
+                @Override
+                public Dialog onCreateDialog(Bundle savedInstanceState) {
+                    return new AlertDialog.Builder(getActivity())
+                        .setMessage(R.string.i2ptunnel_delete_confirm_message)
+                        .setPositiveButton(R.string.i2ptunnel_delete_confirm_button,
+                                new DialogInterface.OnClickListener() {
+
+                                    public void onClick(DialogInterface dialog, int which) {
+                                        List<String> msgs = TunnelUtil.deleteTunnel(
+                                                getActivity(), mGroup, mTunnel.getId());
+                                        dialog.dismiss();
+                                        Toast.makeText(getActivity().getApplicationContext(),
+                                                msgs.get(0), Toast.LENGTH_LONG).show();
+                                        mCallback.onTunnelDeleted(mTunnel.getId(),
+                                                mGroup.getControllers().size());
+                                    }
+                                })
+                        .setNegativeButton(android.R.string.cancel, null)
+                        .create();
+                }
+            };
+            dg.show(getFragmentManager(), "delete_tunnel_dialog");
+            return true;
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java b/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..60f4e40b0420dd788a175bfc4989e0e12b9344df
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java
@@ -0,0 +1,235 @@
+package net.i2p.android.i2ptunnel.fragment;
+
+import java.util.List;
+
+import net.i2p.android.i2ptunnel.activity.TunnelWizardActivity;
+import net.i2p.android.i2ptunnel.adapter.TunnelEntryAdapter;
+import net.i2p.android.i2ptunnel.loader.TunnelEntry;
+import net.i2p.android.i2ptunnel.loader.TunnelEntryLoader;
+import net.i2p.android.i2ptunnel.util.TunnelConfig;
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.HelpActivity;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Toast;
+
+public class TunnelListFragment extends ListFragment
+        implements LoaderManager.LoaderCallbacks<List<TunnelEntry>> {
+    public static final String SHOW_CLIENT_TUNNELS = "show_client_tunnels";
+    public static final String TUNNEL_WIZARD_DATA = "tunnel_wizard_data";
+
+    static final int TUNNEL_WIZARD_REQUEST = 1;
+
+    private static final int CLIENT_LOADER_ID = 1;
+    private static final int SERVER_LOADER_ID = 2;
+    /**
+     * The serialization (saved instance state) Bundle key representing the
+     * activated item position. Only used on tablets.
+     */
+    private static final String STATE_ACTIVATED_POSITION = "activated_position";
+
+    OnTunnelSelectedListener mCallback;
+    private TunnelControllerGroup mGroup;
+    private TunnelEntryAdapter mAdapter;
+    private boolean mClientTunnels;
+    /**
+     * The current activated item position. Only used on tablets.
+     */
+    private int mActivatedPosition = ListView.INVALID_POSITION;
+    private boolean mActivateOnItemClick = false;
+
+    // Container Activity must implement this interface
+    public interface OnTunnelSelectedListener {
+        public void onTunnelSelected(int tunnelId);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // This makes sure that the container activity has implemented
+        // the callback interface. If not, it throws an exception
+        try {
+            mCallback = (OnTunnelSelectedListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement OnTunnelSelectedListener");
+        }
+
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        // Restore the previously serialized activated item position.
+        if (savedInstanceState != null
+                && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
+            setActivatedPosition(savedInstanceState
+                    .getInt(STATE_ACTIVATED_POSITION));
+        }
+
+        // When setting CHOICE_MODE_SINGLE, ListView will automatically
+        // give items the 'activated' state when touched.
+        getListView().setChoiceMode(
+                mActivateOnItemClick ? ListView.CHOICE_MODE_SINGLE
+                        : ListView.CHOICE_MODE_NONE);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mAdapter = new TunnelEntryAdapter(getActivity());
+        mClientTunnels = getArguments().getBoolean(SHOW_CLIENT_TUNNELS);
+
+        String error;
+        try {
+            mGroup = TunnelControllerGroup.getInstance();
+            error = mGroup == null ? getResources().getString(R.string.i2ptunnel_not_initialized) : null;
+        } catch (IllegalArgumentException iae) {
+            mGroup = null;
+            error = iae.toString();
+        }
+
+        if (mGroup == null) {
+            setEmptyText(error);
+        } else {
+            if (mClientTunnels)
+                setEmptyText("No configured client tunnels.");
+            else
+                setEmptyText("No configured server tunnels.");
+        }
+
+        setListAdapter(mAdapter);
+        setListShown(false);
+
+        getLoaderManager().initLoader(mClientTunnels ? CLIENT_LOADER_ID
+                : SERVER_LOADER_ID, null, this);
+    }
+
+    @Override
+    public void onListItemClick(ListView parent, View view, int pos, long id) {
+        super.onListItemClick(parent, view, pos, id);
+        mCallback.onTunnelSelected(mAdapter.getItem(pos).getId());
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        if (mActivatedPosition != ListView.INVALID_POSITION) {
+            // Serialize and persist the activated item position.
+            outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
+        }
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.fragment_i2ptunnel_list_actions, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle presses on the action bar items
+        List<String> msgs;
+        switch (item.getItemId()) {
+        case R.id.action_add_tunnel:
+            Intent wi = new Intent(getActivity(), TunnelWizardActivity.class);
+            startActivityForResult(wi, TUNNEL_WIZARD_REQUEST);
+            return true;
+        case R.id.action_start_all_tunnels:
+            msgs = mGroup.startAllControllers();
+            break;
+        case R.id.action_stop_all_tunnels:
+            msgs = mGroup.stopAllControllers();
+            break;
+        case R.id.action_restart_all_tunnels:
+            msgs = mGroup.restartAllControllers();
+            break;
+        case R.id.action_i2ptunnel_help:
+            Intent hi = new Intent(getActivity(), HelpActivity.class);
+            hi.putExtra(HelpActivity.REFERRER, "i2ptunnel");
+            startActivity(hi);
+            return true;
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+        // TODO: Do something with the other messages
+        if (msgs.size() > 0)
+            Toast.makeText(getActivity().getApplicationContext(),
+                    msgs.get(0), Toast.LENGTH_LONG).show();
+        return true;
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == TUNNEL_WIZARD_REQUEST) {
+            if (resultCode == Activity.RESULT_OK) {
+                Bundle tunnelData = data.getExtras().getBundle(TUNNEL_WIZARD_DATA);
+                TunnelConfig cfg = TunnelConfig.createFromWizard(getActivity(), mGroup, tunnelData);
+                TunnelEntry tunnel = TunnelEntry.createNewTunnel(getActivity(), mGroup, cfg);
+                mAdapter.add(tunnel);
+            }
+        }
+    }
+
+    /**
+     * Turns on activate-on-click mode. When this mode is on, list items will be
+     * given the 'activated' state when touched.
+     */
+    public void setActivateOnItemClick(boolean activateOnItemClick) {
+        mActivateOnItemClick = activateOnItemClick;
+    }
+
+    private void setActivatedPosition(int position) {
+        if (position == ListView.INVALID_POSITION) {
+            getListView().setItemChecked(mActivatedPosition, false);
+        } else {
+            getListView().setItemChecked(position, true);
+        }
+
+        mActivatedPosition = position;
+    }
+
+    // LoaderManager.LoaderCallbacks<List<TunnelEntry>>
+
+    public Loader<List<TunnelEntry>> onCreateLoader(int id, Bundle args) {
+        return new TunnelEntryLoader(getActivity(), mGroup, mClientTunnels);
+    }
+
+    public void onLoadFinished(Loader<List<TunnelEntry>> loader,
+            List<TunnelEntry> data) {
+        if (loader.getId() == (mClientTunnels ?
+                CLIENT_LOADER_ID : SERVER_LOADER_ID)) {
+            mAdapter.setData(data);
+
+            if (isResumed()) {
+                setListShown(true);
+            } else {
+                setListShownNoAnimation(true);
+            }
+        }
+    }
+
+    public void onLoaderReset(Loader<List<TunnelEntry>> loader) {
+        if (loader.getId() == (mClientTunnels ?
+                CLIENT_LOADER_ID : SERVER_LOADER_ID)) {
+            mAdapter.setData(null);
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java b/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0f2913fb380e690d0adf74017679bd9cb54ead8
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java
@@ -0,0 +1,224 @@
+package net.i2p.android.i2ptunnel.loader;
+
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.widget.Toast;
+import net.i2p.android.i2ptunnel.util.TunnelConfig;
+import net.i2p.android.i2ptunnel.util.TunnelUtil;
+import net.i2p.android.router.R;
+import net.i2p.data.Destination;
+import net.i2p.data.PrivateKeyFile;
+import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+
+public class TunnelEntry {
+    public static final int RUNNING = 1;
+    public static final int STARTING = 2;
+    public static final int NOT_RUNNING = 3;
+    public static final int STANDBY = 4;
+
+    private final Context mContext;
+    private final TunnelController mController;
+    private final int mId;
+
+    public static TunnelEntry createNewTunnel(
+            Context ctx,
+            TunnelControllerGroup tcg,
+            TunnelConfig cfg) {
+        int tunnelId = tcg.getControllers().size();
+        List<String> msgs = TunnelUtil.saveTunnel(
+                ctx, tcg, -1, cfg.getConfig());
+        // TODO: Do something else with the other messages.
+        Toast.makeText(ctx.getApplicationContext(),
+                msgs.get(0), Toast.LENGTH_LONG).show();
+        TunnelController cur = TunnelUtil.getController(tcg, tunnelId);
+        return new TunnelEntry(ctx, cur, tunnelId);
+    }
+
+    public TunnelEntry(Context context, TunnelController controller, int id) {
+        mContext = context;
+        mController = controller;
+        mId = id;
+    }
+
+    public int getId() {
+        return mId;
+    }
+
+    public TunnelController getController() {
+        return mController;
+    }
+
+    /* General tunnel data for any type */
+
+    public String getName() {
+        if (mController.getName() != null)
+            return mController.getName();
+        else
+            return mContext.getResources()
+                    .getString(R.string.i2ptunnel_new_tunnel);
+    }
+
+    public String getInternalType() {
+        return mController.getType();
+    }
+
+    public String getType() {
+        return TunnelUtil.getTypeName(mController.getType(), mContext);
+    }
+
+    public String getDescription() {
+        String rv = mController.getDescription();
+        if (rv != null)
+            return rv;
+        return "";
+    }
+
+    public boolean startAutomatically() {
+        return mController.getStartOnLoad();
+    }
+
+    public int getStatus() {
+        if (mController.getIsRunning()) {
+            if (isClient() && mController.getIsStandby())
+                return STANDBY;
+            else
+                return RUNNING;
+        } else if (mController.getIsStarting()) return STARTING;
+        else return NOT_RUNNING;
+    }
+
+    public boolean isClient() {
+        return TunnelUtil.isClient(mController.getType());
+    }
+
+    /* Client tunnel data */
+
+    public boolean isSharedClient() {
+        return Boolean.parseBoolean(mController.getSharedClient());
+    }
+
+    public String getClientInterface() {
+        if ("streamrclient".equals(mController.getType()))
+            return mController.getTargetHost();
+        else
+            return mController.getListenOnInterface();
+    }
+
+    public String getClientPort() {
+        String rv = mController.getListenPort();
+        if (rv != null)
+            return rv;
+        return "";
+    }
+
+    public String getClientDestination() {
+        String rv;
+        if ("client".equals(getInternalType()) ||
+                "ircclient".equals(getInternalType()) ||
+                "streamrclient".equals(getInternalType()))
+            rv = mController.getTargetDestination();
+        else
+            rv = mController.getProxyList();
+        return rv != null ? rv : "";
+    }
+
+    /* Server tunnel data */
+
+    /**
+     * Call this to see if it is okay to linkify getServerTarget()
+     * @return true if getServerTarget() can be linkified, false otherwise.
+     */
+    public boolean isServerTargetLinkValid() {
+        return ("httpserver".equals(mController.getType()) ||
+                "httpbidirserver".equals(mController.getType())) &&
+                mController.getTargetHost() != null &&
+                mController.getTargetPort() != null;
+    }
+
+    /**
+     * @return valid host:port only if isServerTargetLinkValid() is true
+     */
+    public String getServerTarget() {
+        String host;
+        if ("streamrserver".equals(getInternalType()))
+            host = mController.getListenOnInterface();
+        else
+            host = mController.getTargetHost();
+        String port = mController.getTargetPort();
+        if (host == null) host = "";
+        if (port == null) port = "";
+        if (host.indexOf(':') >= 0)
+            host = '[' + host + ']';
+        return host + ":" + port;
+    }
+
+    public String getDestinationBase64() {
+        String rv = mController.getMyDestination();
+        if (rv != null)
+            return rv;
+        // if not running, do this the hard way
+        String keyFile = mController.getPrivKeyFile();
+        if (keyFile != null && keyFile.trim().length() > 0) {
+            PrivateKeyFile pkf = new PrivateKeyFile(keyFile);
+            try {
+                Destination d = pkf.getDestination();
+                if (d != null)
+                    return d.toBase64();
+            } catch (Exception e) {}
+        }
+        return "";
+    }
+
+    public String getDestHashBase32() {
+        String rv = mController.getMyDestHashBase32();
+        if (rv != null)
+            return rv;
+        return "";
+    }
+
+    /* Data for some client and server tunnels */
+
+    /* Other output formats */
+
+    public String getIfacePort() {
+        if (isClient()) {
+            String host;
+            if ("streamrclient".equals(getInternalType()))
+                host = mController.getTargetHost();
+            else
+                host = mController.getListenOnInterface();
+            String port = mController.getListenPort();
+            if (host == null) host = "";
+            if (port == null) port = "";
+            return host + ":" + port;
+        } else return getServerTarget();
+    }
+
+    public String getDetails() {
+        String details;
+        if (isClient())
+            details = getClientDestination();
+        else
+            details = "";
+        return details;
+    }
+
+    public Drawable getStatusIcon() {
+        switch (getStatus()) {
+        case STANDBY:
+        case STARTING:
+            return mContext.getResources()
+                    .getDrawable(R.drawable.local_inprogress);
+        case RUNNING:
+            return mContext.getResources()
+                    .getDrawable(R.drawable.local_up);
+        case NOT_RUNNING:
+        default:
+            return mContext.getResources()
+                    .getDrawable(R.drawable.local_down);
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/loader/TunnelEntryLoader.java b/src/net/i2p/android/i2ptunnel/loader/TunnelEntryLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..e9fe1ae1b789e9dba65710d89db804b5cc6e5f7a
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/loader/TunnelEntryLoader.java
@@ -0,0 +1,140 @@
+package net.i2p.android.i2ptunnel.loader;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+
+import android.content.Context;
+import android.os.Handler;
+import android.support.v4.content.AsyncTaskLoader;
+
+public class TunnelEntryLoader extends AsyncTaskLoader<List<TunnelEntry>> {
+    private TunnelControllerGroup mGroup;
+    private boolean mClientTunnels;
+    private List<TunnelEntry> mData;
+    private Handler mHandler;
+    private TunnelControllerMonitor mMonitor;
+
+    public TunnelEntryLoader(Context context, TunnelControllerGroup tcg, boolean clientTunnels) {
+        super(context);
+        mGroup = tcg;
+        mClientTunnels = clientTunnels;
+        mHandler = new Handler();
+    }
+
+    @Override
+    public List<TunnelEntry> loadInBackground() {
+        List<TunnelEntry> ret = new ArrayList<TunnelEntry>();
+        List<TunnelController> controllers = mGroup.getControllers();
+        for (int i = 0; i < controllers.size(); i++) {
+            TunnelEntry tunnel = new TunnelEntry(getContext(), controllers.get(i), i);
+            if ( (mClientTunnels && tunnel.isClient()) ||
+                 (!mClientTunnels && !tunnel.isClient()) )
+                ret.add(tunnel);
+        }
+        return ret;
+    }
+
+    @Override
+    public void deliverResult(List<TunnelEntry> data) {
+        if (isReset()) {
+            // The Loader has been reset; ignore the result and invalidate the data.
+            if (data != null) {
+                releaseResources(data);
+                return;
+            }
+        }
+
+        // Hold a reference to the old data so it doesn't get garbage collected.
+        // We must protect it until the new data has been delivered.
+        List<TunnelEntry> oldData = mData;
+        mData = data;
+
+        if (isStarted()) {
+            // If the Loader is in a started state, have the superclass deliver the
+            // results to the client.
+            super.deliverResult(data);
+        }
+
+        // Invalidate the old data as we don't need it any more.
+        if (oldData != null && oldData != data) {
+            releaseResources(oldData);
+        }
+    }
+
+    @Override
+    protected void onStartLoading() {
+        if (mData != null) {
+            // Deliver any previously loaded data immediately.
+            deliverResult(mData);
+        }
+
+        // Begin monitoring the underlying data source.
+        mMonitor = new TunnelControllerMonitor();
+        mHandler.postDelayed(mMonitor, 50);
+
+        if (takeContentChanged() || mData == null) {
+            // When the observer detects a change, it should call onContentChanged()
+            // on the Loader, which will cause the next call to takeContentChanged()
+            // to return true. If this is ever the case (or if the current data is
+            // null), we force a new load.
+            forceLoad();
+        }
+    }
+
+    @Override
+    protected void onStopLoading() {
+        // The Loader is in a stopped state, so we should attempt to cancel the 
+        // current load (if there is one).
+        cancelLoad();
+
+        // Note that we leave the observer as is. Loaders in a stopped state
+        // should still monitor the data source for changes so that the Loader
+        // will know to force a new load if it is ever started again.
+    }
+
+    @Override
+    protected void onReset() {
+        // Ensure the loader has been stopped.
+        onStopLoading();
+
+        // At this point we can release the resources associated with 'mData'.
+        if (mData != null) {
+            releaseResources(mData);
+            mData = null;
+        }
+
+        // The Loader is being reset, so we should stop monitoring for changes.
+        if (mMonitor != null) {
+            mHandler.removeCallbacks(mMonitor);
+            mMonitor = null;
+        }
+    }
+
+    @Override
+    public void onCanceled(List<TunnelEntry> data) {
+        // Attempt to cancel the current asynchronous load.
+        super.onCanceled(data);
+
+        // The load has been canceled, so we should release the resources
+        // associated with 'data'.
+        releaseResources(data);
+    }
+
+    private void releaseResources(List<TunnelEntry> data) {
+        // For a simple List, there is nothing to do. For something like a Cursor, we 
+        // would close it in this method. All resources associated with the Loader
+        // should be released here.
+    }
+
+    private class TunnelControllerMonitor implements Runnable {
+        public void run() {
+            // There is no way (yet) to monitor for changes to the list of
+            // TunnelControllers, so just force a refresh every 10 seconds.
+            onContentChanged();
+            mHandler.postDelayed(this, 10 * 1000);
+        }
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java b/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bbb41b367502ca16c5b8926fa4dbb121b2f231d
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java
@@ -0,0 +1,668 @@
+package net.i2p.android.i2ptunnel.util;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+
+import net.i2p.I2PAppContext;
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.i2ptunnel.I2PTunnelConnectClient;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
+import net.i2p.i2ptunnel.I2PTunnelIRCClient;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.PasswordManager;
+
+public class TunnelConfig {
+    protected final I2PAppContext _context;
+
+    private String _type;
+    private String _name;
+    private String _description;
+    private String _i2cpHost;
+    private String _i2cpPort;
+    private String _tunnelDepth;
+    private String _tunnelQuantity;
+    private String _tunnelVariance;
+    private String _tunnelBackupQuantity;
+    private boolean _connectDelay;
+    private String _customOptions;
+    private String _proxyList;
+    private String _port;
+    private String _reachableBy;
+    private String _targetDestination;
+    private String _targetHost;
+    private String _targetPort;
+    private String _spoofedHost;
+    private String _privKeyFile;
+    private String _profile;
+    private boolean _startOnLoad;
+    private boolean _sharedClient;
+    private final Set<String> _booleanOptions;
+    private final Map<String, String> _otherOptions;
+    private String _newProxyUser;
+    private String _newProxyPW;
+
+    static final String CLIENT_NICKNAME = "shared clients";
+
+    public static TunnelConfig createFromWizard(
+            Context ctx, TunnelControllerGroup tcg, Bundle data) {
+        // Get the Bundle keys
+        Resources res = ctx.getResources();
+
+        String kClientServer = res.getString(R.string.i2ptunnel_wizard_k_client_server);
+        String kType = res.getString(R.string.i2ptunnel_wizard_k_type);
+
+        String kName = res.getString(R.string.i2ptunnel_wizard_k_name);
+        String kDesc = res.getString(R.string.i2ptunnel_wizard_k_desc);
+        String kDest = res.getString(R.string.i2ptunnel_wizard_k_dest);
+        String kOutproxies = res.getString(R.string.i2ptunnel_wizard_k_outproxies);
+        String kTargetHost = res.getString(R.string.i2ptunnel_wizard_k_target_host);
+        String kTargetPort = res.getString(R.string.i2ptunnel_wizard_k_target_port);
+        String kReachableOn = res.getString(R.string.i2ptunnel_wizard_k_reachable_on);
+        String kBindingPort = res.getString(R.string.i2ptunnel_wizard_k_binding_port);
+        String kAutoStart = res.getString(R.string.i2ptunnel_wizard_k_auto_start);
+
+        // Create the TunnelConfig
+        TunnelConfig cfg = new TunnelConfig();
+
+        // Get/set the tunnel wizard settings
+        String clientServer = data.getBundle(kClientServer).getString(Page.SIMPLE_DATA_KEY);
+        String typeName = data.getBundle(clientServer + ":" + kType).getString(Page.SIMPLE_DATA_KEY);
+        String type = TunnelUtil.getTypeFromName(typeName, ctx);
+        cfg.setType(type);
+
+        String name = data.getBundle(kName).getString(Page.SIMPLE_DATA_KEY);
+        cfg.setName(name);
+
+        String desc = data.getBundle(kDesc).getString(Page.SIMPLE_DATA_KEY);
+        cfg.setDescription(desc);
+
+        String dest = null;
+        Bundle pageData = data.getBundle(kDest);
+        if (pageData != null) dest = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetDestination(dest);
+
+        String outproxies = null;
+        pageData = data.getBundle(kOutproxies);
+        if (pageData != null) outproxies = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setProxyList(outproxies);
+
+        String targetHost = null;
+        pageData = data.getBundle(kTargetHost);
+        if (pageData != null) targetHost = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetHost(targetHost);
+
+        String targetPort = null;
+        pageData = data.getBundle(kTargetPort);
+        if (pageData != null) targetPort = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetPort(targetPort);
+
+        String reachableOn = null;
+        pageData = data.getBundle(kReachableOn);
+        if (pageData != null) reachableOn = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setReachableBy(reachableOn);
+
+        String bindingPort = null;
+        pageData = data.getBundle(kBindingPort);
+        if (pageData != null) bindingPort = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setPort(bindingPort);
+
+        boolean autoStart = data.getBundle(kAutoStart).getBoolean(Page.SIMPLE_DATA_KEY);
+        cfg.setStartOnLoad(autoStart);
+
+        // Set sensible defaults for a new tunnel
+        cfg.setTunnelDepth("3");
+        cfg.setTunnelVariance("0");
+        cfg.setTunnelQuantity("2");
+        cfg.setTunnelBackupQuantity("0");
+        cfg.setClientHost("internal");
+        cfg.setClientport("internal");
+        cfg.setCustomOptions("");
+        if (!"streamrclient".equals(type)) {
+            cfg.setProfile("bulk");
+            cfg.setReduceCount("1");
+            cfg.setReduceTime("20");
+        }
+        if (TunnelUtil.isClient(type)) { /* Client-only defaults */
+            if (!"streamrclient".equals(type)) {
+                cfg.setNewDest("0");
+                cfg.setCloseTime("30");
+            }
+            if ("httpclient".equals(type) ||
+                    "connectclient".equals(type) ||
+                    "sockstunnel".equals(type) |
+                    "socksirctunnel".equals(type)) {
+                cfg.setProxyUsername("");
+                cfg.setProxyPassword("");
+                cfg.setOutproxyUsername("");
+                cfg.setOutproxyPassword("");
+            }
+            if ("httpclient".equals(type))
+                cfg.setJumpList("http://i2host.i2p/cgi-bin/i2hostjump?\nhttp://stats.i2p/cgi-bin/jump.cgi?a=");
+        } else { /* Server-only defaults */
+            cfg.setPrivKeyFile(TunnelUtil.getPrivateKeyFile(tcg, -1));
+            cfg.setEncrypt("");
+            cfg.setEncryptKey("");
+            cfg.setAccessMode("0");
+            cfg.setAccessList("");
+            cfg.setLimitMinute("0");
+            cfg.setLimitHour("0");
+            cfg.setLimitDay("0");
+            cfg.setTotalMinute("0");
+            cfg.setTotalHour("0");
+            cfg.setTotalDay("0");
+            cfg.setMaxStreams("0");
+        }
+
+        return cfg;
+    }
+
+    public TunnelConfig() {
+        _context = I2PAppContext.getGlobalContext();
+        _booleanOptions = new ConcurrentHashSet<String>(4);
+        _otherOptions = new ConcurrentHashMap<String,String>(4);
+    }
+
+    /**
+     * What type of tunnel (httpclient, ircclient, client, or server).  This is 
+     * required when adding a new tunnel.
+     *
+     */
+    public void setType(String type) { 
+        _type = (type != null ? type.trim() : null);   
+    }
+    String getType() { return _type; }
+
+    /** Short name of the tunnel */
+    public void setName(String name) { 
+        _name = (name != null ? name.trim() : null);
+    }
+    /** one line description */
+    public void setDescription(String description) { 
+        _description = (description != null ? description.trim() : null);
+    }
+    /** I2CP host the router is on, ignored when in router context */
+    public void setClientHost(String host) {
+        _i2cpHost = (host != null ? host.trim() : null);
+    }
+    /** I2CP port the router is on, ignored when in router context */
+    public void setClientport(String port) {
+        _i2cpPort = (port != null ? port.trim() : null);
+    }
+    /** how many hops to use for inbound tunnels */
+    public void setTunnelDepth(String tunnelDepth) { 
+        _tunnelDepth = (tunnelDepth != null ? tunnelDepth.trim() : null);
+    }
+    /** how many parallel inbound tunnels to use */
+    public void setTunnelQuantity(String tunnelQuantity) { 
+        _tunnelQuantity = (tunnelQuantity != null ? tunnelQuantity.trim() : null);
+    }
+    /** how much randomisation to apply to the depth of tunnels */
+    public void setTunnelVariance(String tunnelVariance) { 
+        _tunnelVariance = (tunnelVariance != null ? tunnelVariance.trim() : null);
+    }
+    /** how many tunnels to hold in reserve to guard against failures */
+    public void setTunnelBackupQuantity(String tunnelBackupQuantity) { 
+        _tunnelBackupQuantity = (tunnelBackupQuantity != null ? tunnelBackupQuantity.trim() : null);
+    }
+    /** what I2P session overrides should be used */
+    public void setCustomOptions(String customOptions) { 
+        _customOptions = (customOptions != null ? customOptions.trim() : null);
+    }
+    /** what HTTP outproxies should be used (httpclient specific) */
+    public void setProxyList(String proxyList) { 
+        _proxyList = (proxyList != null ? proxyList.trim() : null);
+    }
+    /** what port should this client/httpclient/ircclient listen on */
+    public void setPort(String port) { 
+        _port = (port != null ? port.trim() : null);
+    }
+    /** 
+     * what interface should this client/httpclient/ircclient listen on
+     */
+    public void setReachableBy(String reachableBy) { 
+        _reachableBy = (reachableBy != null ? reachableBy.trim() : null);
+    }
+    /** What peer does this client tunnel point at */
+    public void setTargetDestination(String dest) { 
+        _targetDestination = (dest != null ? dest.trim() : null);
+    }
+    /** What host does this server tunnel point at */
+    public void setTargetHost(String host) { 
+        _targetHost = (host != null ? host.trim() : null);
+    }
+    /** What port does this server tunnel point at */
+    public void setTargetPort(String port) { 
+        _targetPort = (port != null ? port.trim() : null);
+    }
+    /** What host does this http server tunnel spoof */
+    public void setSpoofedHost(String host) { 
+        _spoofedHost = (host != null ? host.trim() : null);
+    }
+    /** What filename is this server tunnel's private keys stored in */
+    public void setPrivKeyFile(String file) { 
+        _privKeyFile = (file != null ? file.trim() : null);
+    }
+    /**
+     * If called with true, we want this tunnel to start whenever it is
+     * loaded (aka right now and whenever the router is started up)
+     */
+    public void setStartOnLoad(boolean val) {
+        _startOnLoad = val;
+    }
+    public void setShared(boolean val) {
+        _sharedClient=val;
+    }
+    public void setConnectDelay(String moo) {
+        _connectDelay = true;
+    }
+    public void setProfile(String profile) { 
+        _profile = profile; 
+    }
+
+    public void setReduce(String moo) {
+        _booleanOptions.add("i2cp.reduceOnIdle");
+    }
+    public void setClose(String moo) {
+        _booleanOptions.add("i2cp.closeOnIdle");
+    }
+    public void setEncrypt(String moo) {
+        _booleanOptions.add("i2cp.encryptLeaseSet");
+    }
+
+    /** @since 0.8.9 */
+    public void setDCC(String moo) {
+        _booleanOptions.add(I2PTunnelIRCClient.PROP_DCC);
+    }
+
+    protected static final String PROP_ENABLE_ACCESS_LIST = "i2cp.enableAccessList";
+    protected static final String PROP_ENABLE_BLACKLIST = "i2cp.enableBlackList";
+
+    public void setAccessMode(String val) {
+        if ("1".equals(val))
+            _booleanOptions.add(PROP_ENABLE_ACCESS_LIST);
+        else if ("2".equals(val))
+            _booleanOptions.add(PROP_ENABLE_BLACKLIST);
+    }
+
+    public void setDelayOpen(String moo) {
+        _booleanOptions.add("i2cp.delayOpen");
+    }
+    public void setNewDest(String val) {
+        if ("1".equals(val))
+            _booleanOptions.add("i2cp.newDestOnResume");
+        else if ("2".equals(val))
+            _booleanOptions.add("persistentClientKey");
+    }
+
+    public void setReduceTime(String val) {
+        if (val != null) {
+            try {
+                _otherOptions.put("i2cp.reduceIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+    public void setReduceCount(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.reduceQuantity", val.trim());
+    }
+    public void setEncryptKey(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.leaseSetKey", val.trim());
+    }
+
+    public void setAccessList(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.accessList", val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
+    }
+
+    public void setJumpList(String val) {
+        if (val != null)
+            _otherOptions.put(I2PTunnelHTTPClient.PROP_JUMP_SERVERS, val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
+    }
+
+    public void setCloseTime(String val) {
+        if (val != null) {
+            try {
+                _otherOptions.put("i2cp.closeIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
+    /** all proxy auth @since 0.8.2 */
+    public void setProxyAuth(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
+    }
+
+    public void setProxyUsername(String s) {
+        if (s != null)
+            _newProxyUser = s.trim();
+    }
+
+    public void setProxyPassword(String s) {
+        if (s != null)
+            _newProxyPW = s.trim();
+    }
+
+    public void setOutproxyAuth(String s) {
+        _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
+    }
+
+    public void setOutproxyUsername(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER, s.trim());
+    }
+
+    public void setOutproxyPassword(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, s.trim());
+    }
+
+    /** all of these are @since 0.8.3 */
+    protected static final String PROP_MAX_CONNS_MIN = "i2p.streaming.maxConnsPerMinute";
+    protected static final String PROP_MAX_CONNS_HOUR = "i2p.streaming.maxConnsPerHour";
+    protected static final String PROP_MAX_CONNS_DAY = "i2p.streaming.maxConnsPerDay";
+    protected static final String PROP_MAX_TOTAL_CONNS_MIN = "i2p.streaming.maxTotalConnsPerMinute";
+    protected static final String PROP_MAX_TOTAL_CONNS_HOUR = "i2p.streaming.maxTotalConnsPerHour";
+    protected static final String PROP_MAX_TOTAL_CONNS_DAY = "i2p.streaming.maxTotalConnsPerDay";
+    protected static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams";
+
+    public void setLimitMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_MIN, s.trim());
+    }
+
+    public void setLimitHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_HOUR, s.trim());
+    }
+
+    public void setLimitDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_DAY, s.trim());
+    }
+
+    public void setTotalMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_MIN, s.trim());
+    }
+
+    public void setTotalHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_HOUR, s.trim());
+    }
+
+    public void setTotalDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_DAY, s.trim());
+    }
+
+    public void setMaxStreams(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_STREAMS, s.trim());
+    }
+
+    /**
+     * Based on all provided data, create a set of configuration parameters 
+     * suitable for use in a TunnelController.  This will replace (not add to)
+     * any existing parameters, so this should return a comprehensive mapping.
+     *
+     */
+    public Properties getConfig() {
+        Properties config = new Properties();
+        updateConfigGeneric(config);
+        
+        if ((TunnelUtil.isClient(_type) && !"streamrclient".equals(_type)) || "streamrserver".equals(_type)) {
+            // streamrserver uses interface
+            if (_reachableBy != null)
+                config.setProperty("interface", _reachableBy);
+            else
+                config.setProperty("interface", "");
+        } else {
+            // streamrclient uses targetHost
+            if (_targetHost != null)
+                config.setProperty("targetHost", _targetHost);
+        }
+
+        if (TunnelUtil.isClient(_type)) {
+            // generic client stuff
+            if (_port != null)
+                config.setProperty("listenPort", _port);
+            config.setProperty("sharedClient", _sharedClient + "");
+            for (String p : _booleanClientOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            for (String p : _otherClientOpts)
+                if (_otherOptions.containsKey(p))
+                    config.setProperty("option." + p, _otherOptions.get(p));
+        } else {
+            // generic server stuff
+            if (_targetPort != null)
+                config.setProperty("targetPort", _targetPort);
+            for (String p : _booleanServerOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            for (String p : _otherServerOpts)
+                if (_otherOptions.containsKey(p))
+                    config.setProperty("option." + p, _otherOptions.get(p));
+        }
+
+        // generic proxy stuff
+        if ("httpclient".equals(_type) || "connectclient".equals(_type) || 
+            "sockstunnel".equals(_type) ||"socksirctunnel".equals(_type)) {
+            for (String p : _booleanProxyOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            if (_proxyList != null)
+                config.setProperty("proxyList", _proxyList);
+        }
+
+        // Proxy auth including migration to MD5
+        if ("httpclient".equals(_type) || "connectclient".equals(_type)) {
+            // Migrate even if auth is disabled
+            // go get the old from custom options that updateConfigGeneric() put in there
+            String puser = "option." + I2PTunnelHTTPClientBase.PROP_USER;
+            String user = config.getProperty(puser);
+            String ppw = "option." + I2PTunnelHTTPClientBase.PROP_PW;
+            String pw = config.getProperty(ppw);
+            if (user != null && pw != null && user.length() > 0 && pw.length() > 0) {
+                String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
+                              user + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
+                if (config.getProperty(pmd5) == null) {
+                    // not in there, migrate
+                    String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
+                                                              : I2PTunnelConnectClient.AUTH_REALM;
+                    String hex = PasswordManager.md5Hex(realm, user, pw);
+                    if (hex != null) {
+                        config.setProperty(pmd5, hex);
+                        config.remove(puser);
+                        config.remove(ppw);
+                    }
+                }
+            }
+            // New user/password
+            String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH);
+            if (auth != null && !auth.equals("false")) {
+                if (_newProxyUser != null && _newProxyPW != null &&
+                    _newProxyUser.length() > 0 && _newProxyPW.length() > 0) {
+                    String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
+                                  _newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
+                    String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
+                                                              : I2PTunnelConnectClient.AUTH_REALM;
+                    String hex = PasswordManager.md5Hex(realm, _newProxyUser, _newProxyPW);
+                    if (hex != null)
+                        config.setProperty(pmd5, hex);
+                }
+            }
+        }
+
+        if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) {
+            if (_targetDestination != null)
+                config.setProperty("targetDestination", _targetDestination);
+        } else if ("httpserver".equals(_type) || "httpbidirserver".equals(_type)) {
+            if (_spoofedHost != null)
+                config.setProperty("spoofedHost", _spoofedHost);
+        }
+        if ("httpbidirserver".equals(_type)) {
+            if (_port != null)
+                config.setProperty("listenPort", _port);
+            if (_reachableBy != null)
+                config.setProperty("interface", _reachableBy);
+            else if (_targetHost != null)
+                config.setProperty("interface", _targetHost);
+            else
+                config.setProperty("interface", "");
+        }
+
+        if ("ircclient".equals(_type)) {
+            boolean dcc = _booleanOptions.contains(I2PTunnelIRCClient.PROP_DCC);
+            config.setProperty("option." + I2PTunnelIRCClient.PROP_DCC,
+                               "" + dcc);
+            // add some sane server options since they aren't in the GUI (yet)
+            if (dcc) {
+                config.setProperty("option." + PROP_MAX_CONNS_MIN, "3");
+                config.setProperty("option." + PROP_MAX_CONNS_HOUR, "10");
+                config.setProperty("option." + PROP_MAX_TOTAL_CONNS_MIN, "5");
+                config.setProperty("option." + PROP_MAX_TOTAL_CONNS_HOUR, "25");
+            }
+        }
+
+        return config;
+    }
+    
+    private static final String _noShowOpts[] = {
+        "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance",
+        "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity",
+        "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize",
+        I2PTunnelIRCClient.PROP_DCC
+        };
+    private static final String _booleanClientOpts[] = {
+        "i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen"
+        };
+    private static final String _booleanProxyOpts[] = {
+        I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH
+        };
+    private static final String _booleanServerOpts[] = {
+        "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST
+        };
+    private static final String _otherClientOpts[] = {
+        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime",
+        "outproxyUsername", "outproxyPassword",
+        I2PTunnelHTTPClient.PROP_JUMP_SERVERS,
+        I2PTunnelHTTPClientBase.PROP_AUTH
+        };
+    private static final String _otherServerOpts[] = {
+        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList",
+         PROP_MAX_CONNS_MIN, PROP_MAX_CONNS_HOUR, PROP_MAX_CONNS_DAY,
+         PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY,
+         PROP_MAX_STREAMS
+        };
+
+    /**
+     *  do NOT add these to noShoOpts, we must leave them in for HTTPClient and ConnectCLient
+     *  so they will get migrated to MD5
+     *  TODO migrate socks to MD5
+     */
+    private static final String _otherProxyOpts[] = {
+        "proxyUsername", "proxyPassword"
+        };
+
+    protected static final Set<String> _noShowSet = new HashSet<String>(64);
+    protected static final Set<String> _nonProxyNoShowSet = new HashSet<String>(4);
+    static {
+        _noShowSet.addAll(Arrays.asList(_noShowOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanClientOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanProxyOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanServerOpts));
+        _noShowSet.addAll(Arrays.asList(_otherClientOpts));
+        _noShowSet.addAll(Arrays.asList(_otherServerOpts));
+        _nonProxyNoShowSet.addAll(Arrays.asList(_otherProxyOpts));
+    }
+
+    private void updateConfigGeneric(Properties config) {
+        config.setProperty("type", _type);
+        if (_name != null)
+            config.setProperty("name", _name);
+        if (_description != null)
+            config.setProperty("description", _description);
+        if (!_context.isRouterContext()) {
+            if (_i2cpHost != null)
+                config.setProperty("i2cpHost", _i2cpHost);
+            if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) {
+                config.setProperty("i2cpPort", _i2cpPort);
+            } else {
+                config.setProperty("i2cpPort", "7654");
+            }
+        }
+        if (_privKeyFile != null)
+            config.setProperty("privKeyFile", _privKeyFile);
+        
+        if (_customOptions != null) {
+            StringTokenizer tok = new StringTokenizer(_customOptions);
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int eq = pair.indexOf('=');
+                if ( (eq <= 0) || (eq >= pair.length()) )
+                    continue;
+                String key = pair.substring(0, eq);
+                if (_noShowSet.contains(key))
+                    continue;
+                // leave in for HTTP and Connect so it can get migrated to MD5
+                // hide for SOCKS until migrated to MD5
+                if ((!"httpclient".equals(_type)) &&
+                    (! "connectclient".equals(_type)) &&
+                    _nonProxyNoShowSet.contains(key))
+                    continue;
+                String val = pair.substring(eq+1);
+                config.setProperty("option." + key, val);
+            }
+        }
+
+        config.setProperty("startOnLoad", _startOnLoad + "");
+
+        if (_tunnelQuantity != null) {
+            config.setProperty("option.inbound.quantity", _tunnelQuantity);
+            config.setProperty("option.outbound.quantity", _tunnelQuantity);
+        }
+        if (_tunnelDepth != null) {
+            config.setProperty("option.inbound.length", _tunnelDepth);
+            config.setProperty("option.outbound.length", _tunnelDepth);
+        }
+        if (_tunnelVariance != null) {
+            config.setProperty("option.inbound.lengthVariance", _tunnelVariance);
+            config.setProperty("option.outbound.lengthVariance", _tunnelVariance);
+        }
+        if (_tunnelBackupQuantity != null) {
+            config.setProperty("option.inbound.backupQuantity", _tunnelBackupQuantity);
+            config.setProperty("option.outbound.backupQuantity", _tunnelBackupQuantity);
+        }
+        if (_connectDelay)
+            config.setProperty("option.i2p.streaming.connectDelay", "1000");
+        else
+            config.setProperty("option.i2p.streaming.connectDelay", "0");
+        if (TunnelUtil.isClient(_type) && _sharedClient) {
+            config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
+            config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
+        } else if (_name != null) {
+            config.setProperty("option.inbound.nickname", _name);
+            config.setProperty("option.outbound.nickname", _name);
+        }
+        if ("interactive".equals(_profile))
+            // This was 1 which doesn't make much sense
+            // The real way to make it interactive is to make the streaming lib
+            // MessageInputStream flush faster but there's no option for that yet,
+            // Setting it to 16 instead of the default but not sure what good that is either.
+            config.setProperty("option.i2p.streaming.maxWindowSize", "16");
+        else
+            config.remove("option.i2p.streaming.maxWindowSize");
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java b/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd73db5767c54fcba8b325d5bf89653d8ddcb9dc
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java
@@ -0,0 +1,241 @@
+package net.i2p.android.i2ptunnel.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import net.i2p.I2PAppContext;
+import net.i2p.android.router.R;
+import net.i2p.android.router.util.Util;
+import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.util.FileUtil;
+import net.i2p.util.SecureFile;
+
+public abstract class TunnelUtil {
+    public static TunnelController getController(TunnelControllerGroup tcg, int tunnel) {
+        if (tunnel < 0) return null;
+        if (tcg == null) return null;
+        List<TunnelController> controllers = tcg.getControllers();
+        if (controllers.size() > tunnel)
+            return controllers.get(tunnel); 
+        else
+            return null;
+    }
+
+    public static List<String> saveTunnel(Context ctx,
+            TunnelControllerGroup tcg,
+            int tunnelId,
+            Properties config) {
+        // Get current tunnel controller
+        TunnelController cur = getController(tcg, tunnelId);
+
+        if (config == null) {
+            List<String> ret = new ArrayList<String>();
+            ret.add("Invalid params");
+            return ret;
+        }
+        
+        if (cur == null) {
+            // creating new
+            cur = new TunnelController(config, "", true);
+            tcg.addController(cur);
+            if (cur.getStartOnLoad())
+                cur.startTunnelBackground();
+        } else {
+            cur.setConfig(config, "");
+        }
+        // Only modify other shared tunnels
+        // if the current tunnel is shared, and of supported type
+        if (Boolean.parseBoolean(cur.getSharedClient()) && isClient(cur.getType())) {
+            // all clients use the same I2CP session, and as such, use the same I2CP options
+            List<TunnelController> controllers = tcg.getControllers();
+
+            for (int i = 0; i < controllers.size(); i++) {
+                TunnelController c = controllers.get(i);
+
+                // Current tunnel modified by user, skip
+                if (c == cur) continue;
+
+                // Only modify this non-current tunnel
+                // if it belongs to a shared destination, and is of supported type
+                if (Boolean.parseBoolean(c.getSharedClient()) && isClient(c.getType())) {
+                    Properties cOpt = c.getConfig("");
+                    if (config.getProperty("option.inbound.quantity") != null)
+                        cOpt.setProperty("option.inbound.quantity", config.getProperty("option.inbound.quantity"));
+                    if (config.getProperty("option.outbound.quantity") != null)
+                        cOpt.setProperty("option.outbound.quantity", config.getProperty("option.outbound.quantity"));
+                    if (config.getProperty("option.inbound.length") != null)
+                        cOpt.setProperty("option.inbound.length", config.getProperty("option.inbound.length"));
+                    if (config.getProperty("option.outbound.length") != null)
+                        cOpt.setProperty("option.outbound.length", config.getProperty("option.outbound.length"));
+                    if (config.getProperty("option.inbound.lengthVariance") != null)
+                        cOpt.setProperty("option.inbound.lengthVariance", config.getProperty("option.inbound.lengthVariance"));
+                    if (config.getProperty("option.outbound.lengthVariance") != null)
+                        cOpt.setProperty("option.outbound.lengthVariance", config.getProperty("option.outbound.lengthVariance"));
+                    if (config.getProperty("option.inbound.backupQuantity") != null)
+                        cOpt.setProperty("option.inbound.backupQuantity", config.getProperty("option.inbound.backupQuantity"));
+                    if (config.getProperty("option.outbound.backupQuantity") != null)
+                        cOpt.setProperty("option.outbound.backupQuantity", config.getProperty("option.outbound.backupQuantity"));
+                    cOpt.setProperty("option.inbound.nickname", TunnelConfig.CLIENT_NICKNAME);
+                    cOpt.setProperty("option.outbound.nickname", TunnelConfig.CLIENT_NICKNAME);
+                    
+                    c.setConfig(cOpt, "");
+                }
+            }
+        }
+
+        return doSave(ctx, tcg);
+    }
+
+    /**
+     *  Stop the tunnel, delete from config,
+     *  rename the private key file if in the default directory
+     */
+    public static List<String> deleteTunnel(Context ctx, TunnelControllerGroup tcg, int tunnelId) {
+        List<String> msgs;        
+        TunnelController cur = getController(tcg, tunnelId);
+        if (cur == null) {
+            msgs = new ArrayList<String>();
+            msgs.add("Invalid tunnel number");
+            return msgs;
+        }
+        
+        msgs = tcg.removeController(cur);
+        msgs.addAll(doSave(ctx, tcg));
+
+        // Rename private key file if it was a default name in
+        // the default directory, so it doesn't get reused when a new
+        // tunnel is created.
+        // Use configured file name if available, not the one from the form.
+        String pk = cur.getPrivKeyFile();
+        //if (pk == null)
+        //    pk = _privKeyFile;
+        if (pk != null && pk.startsWith("i2ptunnel") && pk.endsWith("-privKeys.dat") &&
+            ((!isClient(cur.getType())) || cur.getPersistentClientKey())) {
+            I2PAppContext context = I2PAppContext.getGlobalContext();
+            File pkf = new File(context.getConfigDir(), pk);
+            if (pkf.exists()) {
+                String name = cur.getName();
+                if (name == null) {
+                    name = cur.getDescription();
+                    if (name == null) {
+                        name = cur.getType();
+                        if (name == null)
+                            name = Long.toString(context.clock().now());
+                    }
+                }
+                name = "i2ptunnel-deleted-" + name.replace(' ', '_') + '-' + context.clock().now() + "-privkeys.dat";
+                File backupDir = new SecureFile(context.getConfigDir(), TunnelController.KEY_BACKUP_DIR);
+                File to;
+                if (backupDir.isDirectory() || backupDir.mkdir())
+                    to = new File(backupDir, name);
+                else
+                    to = new File(context.getConfigDir(), name);
+                boolean success = FileUtil.rename(pkf, to);
+                if (success)
+                    msgs.add("Private key file " + pkf.getAbsolutePath() +
+                             " renamed to " + to.getAbsolutePath());
+            }
+        }
+        return msgs;
+    }
+
+    private static List<String> doSave(Context ctx, TunnelControllerGroup tcg) { 
+        List<String> rv = tcg.clearAllMessages();
+        try {
+            tcg.saveConfig();
+            rv.add(0, ctx.getResources().getString(R.string.i2ptunnel_msg_config_saved));
+        } catch (IOException ioe) {
+            Util.e("Failed to save config file", ioe);
+            rv.add(0, ctx.getResources().getString(R.string.i2ptunnel_msg_config_save_failed) + ": " + ioe.toString());
+        }
+        return rv;
+    }
+
+    /* General tunnel data for any type */
+
+    public static String getTypeFromName(String typeName, Context ctx) {
+        Resources res = ctx.getResources();
+        if (res.getString(R.string.i2ptunnel_type_client).equals(typeName))
+            return "client";
+        else if (res.getString(R.string.i2ptunnel_type_httpclient).equals(typeName))
+            return "httpclient";
+        else if (res.getString(R.string.i2ptunnel_type_ircclient).equals(typeName))
+            return "ircclient";
+        else if (res.getString(R.string.i2ptunnel_type_server).equals(typeName))
+            return "server";
+        else if (res.getString(R.string.i2ptunnel_type_httpserver).equals(typeName))
+            return "httpserver";
+        else if (res.getString(R.string.i2ptunnel_type_sockstunnel).equals(typeName))
+            return "sockstunnel";
+        else if (res.getString(R.string.i2ptunnel_type_socksirctunnel).equals(typeName))
+            return "socksirctunnel";
+        else if (res.getString(R.string.i2ptunnel_type_connectclient).equals(typeName))
+            return "connectclient";
+        else if (res.getString(R.string.i2ptunnel_type_ircserver).equals(typeName))
+            return "ircserver";
+        else if (res.getString(R.string.i2ptunnel_type_streamrclient).equals(typeName))
+            return "streamrclient";
+        else if (res.getString(R.string.i2ptunnel_type_streamrserver).equals(typeName))
+            return "streamrserver";
+        else if (res.getString(R.string.i2ptunnel_type_httpbidirserver).equals(typeName))
+            return "httpbidirserver";
+        else
+            return typeName;
+    }
+
+    public static String getTypeName(String type, Context context) {
+        Resources res = context.getResources();
+        if ("client".equals(type))
+            return res.getString(R.string.i2ptunnel_type_client);
+        else if ("httpclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpclient);
+        else if ("ircclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_ircclient);
+        else if ("server".equals(type))
+            return res.getString(R.string.i2ptunnel_type_server);
+        else if ("httpserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpserver);
+        else if ("sockstunnel".equals(type))
+            return res.getString(R.string.i2ptunnel_type_sockstunnel);
+        else if ("socksirctunnel".equals(type))
+            return res.getString(R.string.i2ptunnel_type_socksirctunnel);
+        else if ("connectclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_connectclient);
+        else if ("ircserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_ircserver);
+        else if ("streamrclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_streamrclient);
+        else if ("streamrserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_streamrserver);
+        else if ("httpbidirserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpbidirserver);
+        else
+            return type;
+    }
+
+    public static boolean isClient(String type) {
+        return ( ("client".equals(type)) ||
+                 ("httpclient".equals(type)) ||
+                 ("sockstunnel".equals(type)) ||
+                 ("socksirctunnel".equals(type)) ||
+                 ("connectclient".equals(type)) ||
+                 ("streamrclient".equals(type)) ||
+                 ("ircclient".equals(type)));
+    }
+
+    public static String getPrivateKeyFile(TunnelControllerGroup tcg, int tunnel) {
+        TunnelController tun = getController(tcg, tunnel);
+        if (tun != null && tun.getPrivKeyFile() != null)
+            return tun.getPrivKeyFile();
+        if (tunnel < 0)
+            tunnel = tcg == null ? 999 : tcg.getControllers().size();
+        return "i2ptunnel" + tunnel + "-privKeys.dat";
+    }
+}
diff --git a/src/net/i2p/android/router/activity/AddressbookActivity.java b/src/net/i2p/android/router/activity/AddressbookActivity.java
index 6d3068acb50ec6188d3410b21193e68dcf78d2e8..094c93dea5086ad9b0d1153c5a8f83102348c840 100644
--- a/src/net/i2p/android/router/activity/AddressbookActivity.java
+++ b/src/net/i2p/android/router/activity/AddressbookActivity.java
@@ -1,97 +1,103 @@
 package net.i2p.android.router.activity;
 
+import net.i2p.android.router.R;
+import net.i2p.android.router.fragment.AddressbookFragment;
+import net.i2p.android.router.fragment.WebFragment;
+import android.app.Activity;
+import android.app.SearchManager;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.v7.app.ActionBarActivity;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.widget.SearchView;
 import android.view.Menu;
-import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
-import android.widget.TextView;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Properties;
-import java.util.Set;
-import net.i2p.I2PAppContext;
-import net.i2p.android.router.R;
-import net.i2p.client.naming.NamingService;
 
-public class AddressbookActivity extends ActionBarActivity {
+public class AddressbookActivity extends I2PActivityBase
+        implements AddressbookFragment.OnAddressSelectedListener,
+        SearchView.OnQueryTextListener {
+    /**
+     * Whether or not the activity is in two-pane mode, i.e. running on a tablet
+     * device.
+     */
+    private boolean mTwoPane;
+
+    @Override
+    protected boolean canUseTwoPanes() {
+        return true;
+    }
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_addressbook);
 
-        // Grab context if router has started, otherwise create new
-        // FIXME dup contexts, locking, ...
-        I2PAppContext ctx = I2PAppContext.getCurrentContext();
-        if (ctx == null) {
-            Properties props = new Properties();
-            String myDir = getFilesDir().getAbsolutePath();
-            props.setProperty("i2p.dir.base", myDir);
-            props.setProperty("i2p.dir.config", myDir);
-            ctx = new I2PAppContext(props);
+        if (findViewById(R.id.detail_fragment) != null) {
+            // The detail container view will be present only in the
+            // large-screen layouts (res/values-large and
+            // res/values-sw600dp). If this view is present, then the
+            // activity should be in two-pane mode.
+            mTwoPane = true;
         }
 
-        // get the names
-        NamingService ns = ctx.namingService();
-        // After router shutdown we get nothing... why?
-        Set<String> names = ns.getNames();
+        // Start with the base view
+        if (savedInstanceState == null) {
+            AddressbookFragment f = new AddressbookFragment();
+            f.setArguments(getIntent().getExtras());
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.main_fragment, f).commit();
+        }
+    }
 
-        // set the header
-        TextView tv = (TextView) getLayoutInflater().inflate(R.layout.addressbook_header, null);
-        int sz = names.size();
-        if (sz > 1)
-            tv.setText(sz + " hosts in address book. Start typing to filter.");
-        else if (sz > 0)
-            tv.setText("1 host in address book.");
-        else
-            tv.setText("No hosts in address book, or your router is not up.");
-        ListView lv = (ListView) findViewById(R.id.addressbook_list);
-        lv.addHeaderView(tv, "", false);
-        lv.setTextFilterEnabled(sz > 1);
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.activity_addressbook_actions, menu);
+        SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
+        MenuItem searchItem = menu.findItem(R.id.action_search_addressbook);
+        SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
+        searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
+        searchView.setOnQueryTextListener(this);
+        return super.onCreateOptionsMenu(menu);
+    }
 
-        // set the list
-        List<String> nameList = new ArrayList<String>(names);
-        Collections.sort(nameList);
-        lv.setAdapter(new ArrayAdapter<String>(this, R.layout.addressbook_list_item, nameList));
+    // AddressbookFragment.OnAddressSelectedListener
 
-        // set the callback
-        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            public void onItemClick(AdapterView parent, View view, int pos, long id) {
-                CharSequence host = ((TextView) view).getText();
-                Intent intent = new Intent(view.getContext(), WebActivity.class);
-                intent.setData(Uri.parse("http://" + host + '/'));
-                startActivity(intent);
-            }
-        });
+    public void onAddressSelected(CharSequence host) {
+        if (getIntent().getAction() == Intent.ACTION_PICK) {
+            Intent result = new Intent();
+            result.setData(Uri.parse("http://" + host));
+            setResult(Activity.RESULT_OK, result);
+            finish();
+        } else {
+            WebFragment f = new WebFragment();
+            Bundle args = new Bundle();
+            args.putString(WebFragment.HTML_URI, "http://" + host + '/');
+            f.setArguments(args);
+            getSupportFragmentManager().beginTransaction()
+                .replace(R.id.main_fragment, f)
+                .addToBackStack(null)
+                .commit();
+        }
     }
 
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-    	MenuInflater inflater = getMenuInflater();
-    	inflater.inflate(R.menu.activity_addressbook_actions, menu);
-    	return super.onCreateOptionsMenu(menu);
+    // SearchView.OnQueryTextListener
+
+    public boolean onQueryTextChange(String newText) {
+        filterAddresses(newText);
+        return true;
     }
 
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        // Handle presses on the action bar items
-        switch (item.getItemId()) {
-            //case R.id.action_add_to_addressbook:
-            //    return true;
-            case R.id.action_addressbook_settings:
-                Intent intent = new Intent(this, AddressbookSettingsActivity.class);
-                startActivity(intent);
-                return true;
-            default:
-                return super.onOptionsItemSelected(item);
+    public boolean onQueryTextSubmit(String query) {
+        filterAddresses(query);
+        return true;
+    }
+
+    private void filterAddresses(String query) {
+        Fragment f = getSupportFragmentManager().findFragmentById(R.id.main_fragment);
+        if (f instanceof AddressbookFragment) {
+            AddressbookFragment af = (AddressbookFragment) f;
+            af.filterAddresses(query);
         }
     }
 }
diff --git a/src/net/i2p/android/router/activity/HelpActivity.java b/src/net/i2p/android/router/activity/HelpActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..79c853d4695d2d7392e6838c71d76bbeadf95a91
--- /dev/null
+++ b/src/net/i2p/android/router/activity/HelpActivity.java
@@ -0,0 +1,52 @@
+package net.i2p.android.router.activity;
+
+import net.i2p.android.router.R;
+import net.i2p.android.router.fragment.TextResourceDialog;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class HelpActivity extends I2PActivityBase {
+    public static final String REFERRER = "help_referrer";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mDrawerToggle.setDrawerIndicatorEnabled(false);
+        /*if (savedInstanceState == null) {
+            HelpFragment f = new HelpFragment();
+            f.setArguments(getIntent().getExtras());
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.main_fragment, f).commit();
+        }*/
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.activity_help_actions, menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+        case R.id.menu_help_licenses:
+            Intent lic = new Intent(HelpActivity.this, LicenseActivity.class);
+            startActivity(lic);
+            return true;
+        case R.id.menu_help_release_notes:
+            TextResourceDialog dialog = new TextResourceDialog();
+            Bundle args = new Bundle();
+            args.putString(TextResourceDialog.TEXT_DIALOG_TITLE,
+                    getResources().getString(R.string.label_release_notes));
+            args.putInt(TextResourceDialog.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
+            dialog.setArguments(args);
+            dialog.show(getSupportFragmentManager(), "release_notes");
+            return true;
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+    }
+}
diff --git a/src/net/i2p/android/router/activity/I2PActivityBase.java b/src/net/i2p/android/router/activity/I2PActivityBase.java
index 5b04f77bff27caa9c3e13189e7e638f4c11154ff..9c3d606b4a7274d2d5c744dda252f2d815ab130c 100644
--- a/src/net/i2p/android/router/activity/I2PActivityBase.java
+++ b/src/net/i2p/android/router/activity/I2PActivityBase.java
@@ -4,26 +4,47 @@ import android.content.ComponentName;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.content.SharedPreferences;
+import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
 import android.support.v7.app.ActionBarActivity;
+import android.support.v7.app.ActionBar.Tab;
 import android.view.Menu;
-import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import net.i2p.android.i2ptunnel.activity.TunnelListActivity;
 import net.i2p.android.router.R;
 import net.i2p.android.router.binder.RouterBinder;
+import net.i2p.android.router.fragment.I2PFragmentBase;
 import net.i2p.android.router.service.RouterService;
 import net.i2p.android.router.util.Util;
-import net.i2p.router.CommSystemFacade;
-import net.i2p.router.NetworkDatabaseFacade;
-import net.i2p.router.Router;
 import net.i2p.router.RouterContext;
-import net.i2p.router.TunnelManagerFacade;
-import net.i2p.router.peermanager.ProfileOrganizer;
-import net.i2p.router.transport.FIFOBandwidthLimiter;
-import net.i2p.stat.StatManager;
 
-public abstract class I2PActivityBase extends ActionBarActivity {
+public abstract class I2PActivityBase extends ActionBarActivity implements
+        I2PFragmentBase.RouterContextProvider {
+    /**
+     * Navigation drawer variables
+     */
+    protected DrawerLayout mDrawerLayout;
+    protected ListView mDrawerList;
+    protected ActionBarDrawerToggle mDrawerToggle;
+
+    private CharSequence mDrawerTitle;
+    private CharSequence mTitle;
+    private String[] mActivityTitles;
+
+    /**
+     * Router variables
+     */
     protected String _myDir;
     protected boolean _isBound;
     protected boolean _triedBind;
@@ -35,7 +56,15 @@ public abstract class I2PActivityBase extends ActionBarActivity {
     protected static final String PREF_AUTO_START = "autoStart";
     /** true leads to a poor install experience, very slow to paint the screen */
     protected static final boolean DEFAULT_AUTO_START = false;
-    protected static final String PREF_INSTALLED_VERSION = "app.version";
+
+    /**
+     * Override this in subclasses that can use two panes, such as a
+     * list/detail class. 
+     * @return whether this Activity can use a two-pane layout.
+     */
+    protected boolean canUseTwoPanes() {
+        return false;
+    }
 
     /** Called when the activity is first created. */
     @Override
@@ -43,7 +72,90 @@ public abstract class I2PActivityBase extends ActionBarActivity {
     {
         Util.i(this + " onCreate called");
         super.onCreate(savedInstanceState);
+        _sharedPrefs = getSharedPreferences(SHARED_PREFS, 0);
         _myDir = getFilesDir().getAbsolutePath();
+
+        // If the Activity can make use of two panes (if available),
+        // load the layout that will enable them. Otherwise, load the
+        // layout that will only ever have a single pane.
+        if (canUseTwoPanes())
+            setContentView(R.layout.activity_navdrawer);
+        else
+            setContentView(R.layout.activity_navdrawer_onepane);
+
+        mTitle = mDrawerTitle = getTitle();
+        mActivityTitles = getResources().getStringArray(R.array.navdrawer_activity_titles);
+        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+        mDrawerList = (ListView) findViewById(R.id.drawer);
+
+        // Set a custom shadow that overlays the main content when the drawer opens
+        mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+        mDrawerList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        // Set the adapter for the list view
+        mDrawerList.setAdapter(new ArrayAdapter<String>(this,
+                android.R.layout.simple_list_item_1, mActivityTitles));
+        // Set the list's click listener
+        mDrawerList.setOnItemClickListener(new DrawerItemClickListener());
+
+        // Enable ActionBar app icon to behave as action to toggle nav drawer
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setHomeButtonEnabled(true);
+
+        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,
+                R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) {
+
+            /** Called when a drawer has settled in a completely closed state. */
+            public void onDrawerClosed(View view) {
+                getSupportActionBar().setTitle(mTitle);
+                supportInvalidateOptionsMenu();
+            }
+
+            /** Called when a drawer has settled in a completely open state. */
+            public void onDrawerOpened(View view) {
+                getSupportActionBar().setTitle(mDrawerTitle);
+                supportInvalidateOptionsMenu();
+            }
+        };
+
+        // Set the drawer toggle as the DrawerListener
+        mDrawerLayout.setDrawerListener(mDrawerToggle);
+    }
+
+    private class DrawerItemClickListener implements ListView.OnItemClickListener {
+        public void onItemClick(AdapterView<?> parent, View view, int pos, long id) {
+            selectItem(pos);
+        }
+    }
+
+    private void selectItem(int pos) {
+        switch (pos) {
+        case 1:
+            Intent ab = new Intent(I2PActivityBase.this, AddressbookActivity.class);
+            startActivity(ab);
+            break;
+        case 2:
+            Intent itb = new Intent(I2PActivityBase.this, TunnelListActivity.class);
+            startActivity(itb);
+            break;
+        case 3:
+            Intent log = new Intent(I2PActivityBase.this, LogActivity.class);
+            startActivity(log);
+            break;
+        case 4:
+            Intent err = new Intent(I2PActivityBase.this, LogActivity.class);
+            err.putExtra(LogActivity.ERRORS_ONLY, true);
+            startActivity(err);
+            break;
+        case 5:
+            Intent peers = new Intent(I2PActivityBase.this, PeersActivity.class);
+            startActivity(peers);
+            break;
+        default:
+            Intent main = new Intent(I2PActivityBase.this, MainActivity.class);
+            startActivity(main);
+            break;
+        }
+        mDrawerLayout.closeDrawer(mDrawerList);
     }
 
     @Override
@@ -58,7 +170,6 @@ public abstract class I2PActivityBase extends ActionBarActivity {
     {
         Util.i(this + " onStart called");
         super.onStart();
-        _sharedPrefs = getSharedPreferences(SHARED_PREFS, 0);
         if (_sharedPrefs.getBoolean(PREF_AUTO_START, DEFAULT_AUTO_START))
             startRouter();
         else
@@ -66,19 +177,19 @@ public abstract class I2PActivityBase extends ActionBarActivity {
     }
 
     /** @param def default */
-    protected String getPref(String pref, String def) {
+    public String getPref(String pref, String def) {
         return _sharedPrefs.getString(pref, def);
     }
 
     /** @return success */
-    protected boolean setPref(String pref, boolean val) {
+    public boolean setPref(String pref, boolean val) {
         SharedPreferences.Editor edit = _sharedPrefs.edit();
         edit.putBoolean(pref, val);
         return edit.commit();
     }
 
     /** @return success */
-    protected boolean setPref(String pref, String val) {
+    public boolean setPref(String pref, String val) {
         SharedPreferences.Editor edit = _sharedPrefs.edit();
         edit.putString(pref, val);
         return edit.commit();
@@ -120,85 +231,75 @@ public abstract class I2PActivityBase extends ActionBarActivity {
         super.onDestroy();
     }
 
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.menu1, menu);
-        return true;
-    }
-
+    /**
+     * Called whenever we call invalidateOptionsMenu()
+     */
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        // add/hide items here
-        RouterService svc = _routerService;
-        boolean showStart = ((svc == null) || (!_isBound) || svc.canManualStart()) &&
-                            Util.isConnected(this);
-        MenuItem start = menu.findItem(R.id.menu_start);
-        start.setVisible(showStart);
-        start.setEnabled(showStart);
-
-        boolean showStop = svc != null && _isBound && svc.canManualStop();
-        MenuItem stop = menu.findItem(R.id.menu_stop);
-        stop.setVisible(showStop);
-        stop.setEnabled(showStop);
-
-        boolean showHome = ! (this instanceof MainActivity);
-        MenuItem home = menu.findItem(R.id.menu_home);
-        home.setVisible(showHome);
-        home.setEnabled(showHome);
-
-        boolean showAddressbook = (this instanceof WebActivity);
-        MenuItem addressbook = menu.findItem(R.id.menu_addressbook);
-        addressbook.setVisible(showAddressbook);
-        addressbook.setEnabled(showAddressbook);
-
-        boolean showReload = showAddressbook || (this instanceof PeersActivity);
-        MenuItem reload = menu.findItem(R.id.menu_reload);
-        reload.setVisible(showReload);
-        reload.setEnabled(showReload);
+        // If the nav drawer is open, hide action items related to the content view
+        boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList);
+        onDrawerChange(drawerOpen);
 
         return super.onPrepareOptionsMenu(menu);
     }
 
+    /**
+     * Override in subclass with e.g.
+     * menu.findItem(R.id.action_add_to_addressbook).setVisible(!drawerOpen);
+     * @param drawerOpen true if the drawer is open
+     */
+    protected void onDrawerChange(boolean drawerOpen) {
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-        case R.id.menu_settings:
-            Intent intent = new Intent(I2PActivityBase.this, SettingsActivity.class);
-            startActivity(intent);
+        // The action bar home/up action should open or close the drawer.
+        // ActionBarDrawerToggle will take care of this.
+        if(mDrawerToggle.onOptionsItemSelected(item)) {
             return true;
+        }
 
-        case R.id.menu_home:
-            Intent i2 = new Intent(I2PActivityBase.this, MainActivity.class);
-            startActivity(i2);
-            return true;
+        // Handle action buttons and overflow
+        return super.onOptionsItemSelected(item);
+    }
 
-        case R.id.menu_addressbook:
-            Intent i3 = new Intent(I2PActivityBase.this, AddressbookActivity.class);
-            startActivity(i3);
-            return true;
+    @Override
+    public void setTitle(CharSequence title) {
+        mTitle = title;
+        getSupportActionBar().setTitle(mTitle);
+    }
 
-        case R.id.menu_start:
-            RouterService svc = _routerService;
-            if (svc != null && _isBound && svc.canManualStart()) {
-                setPref(PREF_AUTO_START, true);
-                svc.manualStart();
-            } else {
-                startRouter();
-            }
-            return true;
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        // Sync the toggle state after onRestoreInstanceState has occurred.
+        mDrawerToggle.syncState();
+    }
 
-        case R.id.menu_stop:
-            RouterService rsvc = _routerService;
-            if (rsvc != null && _isBound && rsvc.canManualStop()) {
-                setPref(PREF_AUTO_START, false);
-                rsvc.manualStop();
-            }
-            return true;
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        // Pass any configuration change to the drawer toggle
+        mDrawerToggle.onConfigurationChanged(newConfig);
+    }
 
-        case R.id.menu_reload:  // handled in WebActivity
-        default:
-            return super.onOptionsItemSelected(item);
+    public static class TabListener implements ActionBar.TabListener {
+        private Fragment mFragment;
+
+        public TabListener(Fragment fragment) {
+            mFragment = fragment;
+        }
+
+        public void onTabSelected(Tab tab, FragmentTransaction ft) {
+            ft.replace(R.id.main_fragment, mFragment);
+        }
+
+        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+            ft.remove(mFragment);
+        }
+
+        public void onTabReselected(Tab tab, FragmentTransaction ft) {
+            // User selected the already selected tab.
         }
     }
 
@@ -271,61 +372,12 @@ public abstract class I2PActivityBase extends ActionBarActivity {
     /** callback from ServiceConnection, override as necessary */
     protected void onRouterUnbind() {}
 
-    ////// Router stuff
+    // I2PFragmentBase.RouterContextProvider
 
-    protected RouterContext getRouterContext() {
+    public RouterContext getRouterContext() {
         RouterService svc = _routerService;
         if (svc == null || !_isBound)
             return null;
         return svc.getRouterContext();
     }
-
-    protected Router getRouter() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.router();
-    }
-
-    protected NetworkDatabaseFacade getNetDb() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.netDb();
-    }
-
-    protected ProfileOrganizer getProfileOrganizer() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.profileOrganizer();
-    }
-
-    protected TunnelManagerFacade getTunnelManager() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.tunnelManager();
-    }
-
-    protected CommSystemFacade getCommSystem() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.commSystem();
-    }
-
-    protected FIFOBandwidthLimiter getBandwidthLimiter() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.bandwidthLimiter();
-    }
-
-    protected StatManager getStatManager() {
-        RouterContext ctx = getRouterContext();
-        if (ctx == null)
-            return null;
-        return ctx.statManager();
-    }
 }
diff --git a/src/net/i2p/android/router/activity/LicenseActivity.java b/src/net/i2p/android/router/activity/LicenseActivity.java
index c124559c09c102a25e889eaf5376de0e9d72f98a..c65bcfda2d7ad9c4786ca6337ffedbcebff7bea1 100644
--- a/src/net/i2p/android/router/activity/LicenseActivity.java
+++ b/src/net/i2p/android/router/activity/LicenseActivity.java
@@ -1,45 +1,20 @@
 package net.i2p.android.router.activity;
 
-import android.app.ListActivity;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.ListView;
 import net.i2p.android.router.R;
+import net.i2p.android.router.fragment.LicenseFragment;
+import android.os.Bundle;
 
-public class LicenseActivity extends ListActivity {
-
-    private static final String[] names = {
-        "Android Application License", "Apache 2.0",
-        "Router License Overview", "Blockfile", "Crypto Filters", "ElGamal / DSA",
-        "GPLv2", "LGPLv2.1", "GPLv3", "LGPLv3", "FatCowIcons",
-        "Ministreaming",
-        "InstallCert", "SHA-256", "SNTP", "Addressbook"};
-
-    private static final int[] files = {
-        R.raw.license_app_txt, R.raw.license_apache20_txt,
-        R.raw.licenses_txt, R.raw.license_blockfile_txt, R.raw.license_bsd_txt, R.raw.license_elgamaldsa_txt,
-        R.raw.license_gplv2_txt, R.raw.license_lgplv2_1_txt, R.raw.license_gplv3_txt, R.raw.license_lgplv3_txt,
-        R.raw.license_fatcowicons_txt, R.raw.license_bsd_txt,
-        R.raw.license_installcert_txt, R.raw.license_sha256_txt, R.raw.license_sntp_txt, R.raw.license_addressbook_txt};
-
+public class LicenseActivity extends I2PActivityBase {
     @Override
-    public void onCreate(Bundle savedInstanceState)
-    {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, names));
-        ListView lv = getListView();
-
-        // set the callback
-        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            public void onItemClick(AdapterView parent, View view, int pos, long id) {
-                Intent intent = new Intent(view.getContext(), TextResourceActivity.class);
-                intent.putExtra(TextResourceActivity.TEXT_RESOURCE_ID, files[pos]);
-                startActivity(intent);
-            }
-        });
+        mDrawerToggle.setDrawerIndicatorEnabled(false);
+        // Start with the base view
+        if (savedInstanceState == null) {
+            LicenseFragment f = new LicenseFragment();
+            f.setArguments(getIntent().getExtras());
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.main_fragment, f).commit();
+        }
     }
 }
diff --git a/src/net/i2p/android/router/activity/LogActivity.java b/src/net/i2p/android/router/activity/LogActivity.java
index a59637ca2b7ef690b9d44605679df15a3b09c732..a1e9d85b3d48b665e18a7a7880bb18c20cacfd6e 100644
--- a/src/net/i2p/android/router/activity/LogActivity.java
+++ b/src/net/i2p/android/router/activity/LogActivity.java
@@ -20,7 +20,7 @@ public class LogActivity extends ListActivity {
     private ArrayAdapter<String> _adap;
     private TextView _headerView;
 
-    final static String ERRORS_ONLY = "errors_only";
+    public final static String ERRORS_ONLY = "errors_only";
     private static final int MAX = 250;
 
     @Override
diff --git a/src/net/i2p/android/router/activity/MainActivity.java b/src/net/i2p/android/router/activity/MainActivity.java
index c0af403a93e92d711982da1eebc4bca4c64f6236..649ec231508b1dd3ba3730635bfa32da1385df1a 100644
--- a/src/net/i2p/android/router/activity/MainActivity.java
+++ b/src/net/i2p/android/router/activity/MainActivity.java
@@ -1,508 +1,106 @@
 package net.i2p.android.router.activity;
 
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.DialogInterface;
+import java.io.File;
+
 import android.content.Intent;
-import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-import java.io.File;
-import java.text.DecimalFormat;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import net.i2p.android.router.R;
+import net.i2p.android.router.fragment.MainFragment;
+import net.i2p.android.router.fragment.VersionDialog;
 import net.i2p.android.router.service.RouterService;
 import net.i2p.android.router.util.Util;
-import net.i2p.data.DataHelper;
-import net.i2p.router.RouterContext;
-
-public class MainActivity extends I2PActivityBase {
-
-    private Handler _handler;
-    private Runnable _updater;
-    private Runnable _oneShotUpdate;
-    private String _savedStatus;
-    private String _ourVersion;
-    private boolean _keep = true;
-    private boolean _startPressed = false;
-    protected static final String PROP_NEW_INSTALL = "i2p.newInstall";
-    protected static final String PROP_NEW_VERSION = "i2p.newVersion";
-    protected static final int DIALOG_NEW_INSTALL = 0;
-    protected static final int DIALOG_NEW_VERSION = 1;
 
-    @Override
-    public void onPostCreate(Bundle savedInstanceState) {
-        Util.i("Initializing...");
-        InitActivities init = new InitActivities(this);
-        init.debugStuff();
-        init.initialize();
-        super.onPostCreate(savedInstanceState);
-        _ourVersion = Util.getOurVersion(this);
-    }
-
-    /**
-     * Called when the activity is first created.
-     */
+public class MainActivity extends I2PActivityBase implements
+        MainFragment.RouterControlListener,
+        VersionDialog.VersionDialogListener {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        // Init stuff here so settings work.
-        _myDir = getFilesDir().getAbsolutePath();
-        if(savedInstanceState != null) {
-            String saved = savedInstanceState.getString("status");
-            if(saved != null) {
-                _savedStatus = saved;
-            }
-        }
-
-        _keep = true;
-        setContentView(R.layout.main);
-
-        Button b = (Button) findViewById(R.id.news_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), NewsActivity.class);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.releasenotes_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), TextResourceActivity.class);
-                intent.putExtra(TextResourceActivity.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.licenses_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), LicenseActivity.class);
-                //Intent intent = new Intent(view.getContext(), TextResourceActivity.class);
-                //intent.putExtra(TextResourceActivity.TEXT_RESOURCE_ID, R.raw.licenses_txt);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.website_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), WebActivity.class);
-                //intent.setData((new Uri.Builder()).scheme("http").authority("www.i2p2.de").path("/").build());
-                intent.setData(Uri.parse("http://www.i2p2.de/"));
-                startActivity(intent);
-            }
-        });
 
-        b = (Button) findViewById(R.id.faq_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), WebActivity.class);
-                //intent.setData((new Uri.Builder()).scheme("http").authority("www.i2p2.de").path("/faq").build());
-                intent.setData(Uri.parse("http://www.i2p2.de/faq"));
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.welcome_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), WebActivity.class);
-                intent.putExtra(WebActivity.HTML_RESOURCE_ID, R.raw.welcome_html);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.addressbook_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), AddressbookActivity.class);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.logs_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), LogActivity.class);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.error_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), LogActivity.class);
-                intent.putExtra(LogActivity.ERRORS_ONLY, true);
-                startActivity(intent);
-            }
-        });
-
-        b = (Button) findViewById(R.id.peers_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                Intent intent = new Intent(view.getContext(), PeersActivity.class);
-                startActivity(intent);
-            }
-        });
-
-        /*
-         * hidden, unused b = (Button) findViewById(R.id.router_stop_button);
-         * b.setOnClickListener(new View.OnClickListener() { public void
-         * onClick(View view) { RouterService svc = _routerService; if (svc !=
-         * null && _isBound) { setPref(PREF_AUTO_START, false);
-         * svc.manualStop(); updateOneShot(); } } });
-         */
-
-        b = (Button) findViewById(R.id.router_start_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                _startPressed = true;
-                RouterService svc = _routerService;
-                if(svc != null && _isBound) {
-                    setPref(PREF_AUTO_START, true);
-                    svc.manualStart();
-                } else {
-                    (new File(_myDir, "wrapper.log")).delete();
-                    startRouter();
-                }
-                updateOneShot();
-            }
-        });
-
-        b = (Button) findViewById(R.id.router_quit_button);
-        b.setOnClickListener(new View.OnClickListener() {
-
-            public void onClick(View view) {
-                RouterService svc = _routerService;
-                if(svc != null && _isBound) {
-                    setPref(PREF_AUTO_START, false);
-                    svc.manualQuit();
-                    updateOneShot();
-                }
-            }
-        });
-
-        _handler = new Handler();
-        _updater = new Updater();
-        _oneShotUpdate = new OneShotUpdate();
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        _handler.removeCallbacks(_updater);
-        _handler.removeCallbacks(_oneShotUpdate);
-        if(_savedStatus != null) {
-            TextView tv = (TextView) findViewById(R.id.main_status_text);
-            tv.setText(_savedStatus);
+        // Start with the home view
+        if (savedInstanceState == null) {
+            MainFragment mainFragment = new MainFragment();
+            mainFragment.setArguments(getIntent().getExtras());
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.main_fragment, mainFragment).commit();
         }
-        checkDialog();
-        _handler.postDelayed(_updater, 100);
     }
 
     @Override
-    public void onStop() {
-        super.onStop();
-        _handler.removeCallbacks(_updater);
-        _handler.removeCallbacks(_oneShotUpdate);
+    protected void onPostCreate(Bundle savedInstanceState) {
+        Util.i("Initializing...");
+        InitActivities init = new InitActivities(this);
+        init.debugStuff();
+        init.initialize();
+        super.onPostCreate(savedInstanceState);
     }
 
     @Override
-    public void onResume() {
-        super.onResume();
-        updateOneShot();
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.activity_main_actions, menu);
+        return super.onCreateOptionsMenu(menu);
     }
 
     @Override
-    public void onSaveInstanceState(Bundle outState) {
-        if(_savedStatus != null) {
-            outState.putString("status", _savedStatus);
-        }
-        super.onSaveInstanceState(outState);
-    }
-
-    private void updateOneShot() {
-        _handler.postDelayed(_oneShotUpdate, 100);
-    }
-
-    private class OneShotUpdate implements Runnable {
-
-        public void run() {
-            updateVisibility();
-            updateStatus();
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+        case R.id.menu_settings:
+            Intent intent = new Intent(MainActivity.this, SettingsActivity.class);
+            startActivity(intent);
+            return true;
+
+        case R.id.menu_help:
+            Intent hi = new Intent(MainActivity.this, HelpActivity.class);
+            hi.putExtra(HelpActivity.REFERRER, "main");
+            startActivity(hi);
+            return true;
+
+        default:
+            return super.onOptionsItemSelected(item);
         }
     }
 
-    private class Updater implements Runnable {
-
-        private int counter;
-        private final int delay = 1000;
-        private final int toloop = delay / 500;
-        public void run() {
-            updateVisibility();
-            if(counter++ % toloop == 0) {
-                updateStatus();
-            }
-            //_handler.postDelayed(this, 2500);
-            _handler.postDelayed(this, delay);
-        }
-    }
+    // MainFragment.RouterControlListener
 
-    private void updateVisibility() {
+    public boolean shouldShowStart() {
         RouterService svc = _routerService;
-        boolean showStart = ((svc == null) || (!_isBound) || svc.canManualStart())
+        return ((svc == null) || (!_isBound) || svc.canManualStart())
                 && Util.isConnected(this);
-        Button start = (Button) findViewById(R.id.router_start_button);
-        start.setVisibility(showStart ? View.VISIBLE : View.INVISIBLE);
-
-        boolean showStop = svc != null && _isBound && svc.canManualStop();
-        // Old stop but leave in memory. Always hide for now.
-        // Button stop = (Button) findViewById(R.id.router_stop_button);
-        // stop.setVisibility( /* showStop ? View.VISIBLE : */ View.INVISIBLE);
-
-        Button quit = (Button) findViewById(R.id.router_quit_button);
-        quit.setVisibility(showStop ? View.VISIBLE : View.INVISIBLE);
-    }
-
-    @Override
-    public void onBackPressed() {
-        RouterContext ctx = getRouterContext();
-        // RouterService svc = _routerService; Which is better to use?!
-        _keep = Util.isConnected(this) && (ctx != null || _startPressed);
-        Util.d("*********************************************************");
-        Util.d("Back pressed, Keep? " + _keep);
-        Util.d("*********************************************************");
-        super.onBackPressed();
     }
 
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        if(!_keep) {
-            Thread t = new Thread(new KillMe());
-            t.start();
-        }
-    }
-
-    private class KillMe implements Runnable {
-
-        public void run() {
-            Util.d("*********************************************************");
-            Util.d("KillMe started!");
-            Util.d("*********************************************************");
-            try {
-                Thread.sleep(500); // is 500ms long enough?
-            } catch(InterruptedException ex) {
-            }
-            System.exit(0);
-        }
+    public boolean shouldShowStop() {
+        RouterService svc = _routerService;
+        return svc != null && _isBound && svc.canManualStop();
     }
 
-    private void updateStatus() {
-        RouterContext ctx = getRouterContext();
-        TextView tv = (TextView) findViewById(R.id.main_status_text);
-
-        if(!Util.isConnected(this)) {
-            tv.setText("Router version: " + _ourVersion + "\nNo Internet connection is available");
-            tv.setVisibility(View.VISIBLE);
-        } else if(ctx != null) {
-            if(_startPressed) {
-                _startPressed = false;
-            }
-            short reach = ctx.commSystem().getReachabilityStatus();
-            int active = ctx.commSystem().countActivePeers();
-            int known = Math.max(ctx.netDb().getKnownRouters() - 1, 0);
-            int inEx = ctx.tunnelManager().getFreeTunnelCount();
-            int outEx = ctx.tunnelManager().getOutboundTunnelCount();
-            int inCl = ctx.tunnelManager().getInboundClientTunnelCount();
-            int outCl = ctx.tunnelManager().getOutboundClientTunnelCount();
-            int part = ctx.tunnelManager().getParticipatingCount();
-            double dLag = ctx.statManager().getRate("jobQueue.jobLag").getRate(60000).getAverageValue();
-            String jobLag = DataHelper.formatDuration((long) dLag);
-            String msgDelay = DataHelper.formatDuration(ctx.throttle().getMessageDelay());
-            String uptime = DataHelper.formatDuration(ctx.router().getUptime());
-
-            String netstatus = "Unknown";
-            if(reach == net.i2p.router.CommSystemFacade.STATUS_DIFFERENT) {
-                netstatus = "Different";
-            }
-            if(reach == net.i2p.router.CommSystemFacade.STATUS_HOSED) {
-                netstatus = "Hosed";
-            }
-            if(reach == net.i2p.router.CommSystemFacade.STATUS_OK) {
-                netstatus = "OK";
-            }
-            if(reach == net.i2p.router.CommSystemFacade.STATUS_REJECT_UNSOLICITED) {
-                netstatus = "Reject Unsolicited";
-            }
-            String tunnelStatus = ctx.throttle().getTunnelStatus();
-            //ctx.commSystem().getReachabilityStatus();
-            double inBW = ctx.bandwidthLimiter().getReceiveBps() / 1024;
-            double outBW = ctx.bandwidthLimiter().getSendBps() / 1024;
-
-            // control total width
-            DecimalFormat fmt;
-            if(inBW >= 1000 || outBW >= 1000) {
-                fmt = new DecimalFormat("#0");
-            } else if(inBW >= 100 || outBW >= 100) {
-                fmt = new DecimalFormat("#0.0");
-            } else {
-                fmt = new DecimalFormat("#0.00");
-            }
-
-            double kBytesIn = ctx.bandwidthLimiter().getTotalAllocatedInboundBytes() / 1024;
-            double kBytesOut = ctx.bandwidthLimiter().getTotalAllocatedOutboundBytes() / 1024;
-
-            // control total width
-            DecimalFormat kBfmt;
-            if(kBytesIn >= 1000 || kBytesOut >= 1000) {
-                kBfmt = new DecimalFormat("#0");
-            } else if(kBytesIn >= 100 || kBytesOut >= 100) {
-                kBfmt = new DecimalFormat("#0.0");
-            } else {
-                kBfmt = new DecimalFormat("#0.00");
-            }
-
-            String status =
-                    "ROUTER STATUS"
-                    + "\nNetwork: " + netstatus
-                    + "\nPeers active/known: " + active + " / " + known
-                    + "\nExploratory Tunnels in/out: " + inEx + " / " + outEx
-                    + "\nClient Tunnels in/out: " + inCl + " / " + outCl;
-
-
-            // Need to see if we have the participation option set to on.
-            // I thought there was a router method for that? I guess not! WHY NOT?
-            // It would be easier if we had a number to test status.
-            String participate = "\nParticipation: " + tunnelStatus +" (" + part + ")";
-
-            String details =
-                    "\nBandwidth in/out: " + fmt.format(inBW) + " / " + fmt.format(outBW) + " KBps"
-                    + "\nData usage in/out: " + kBfmt.format(kBytesIn) + " / " + kBfmt.format(kBytesOut) + " KB"
-                    + "\nMemory: " + DataHelper.formatSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
-                    + "B / " + DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B'
-                    + "\nJob Lag: " + jobLag
-                    + "\nMsg Delay: " + msgDelay
-                    + "\nUptime: " + uptime;
-
-            _savedStatus = "Router version: " + _ourVersion + "\n" + status + participate + details;
-            tv.setText(_savedStatus);
-            tv.setVisibility(View.VISIBLE);
+    public void onStartRouterClicked() {
+        RouterService svc = _routerService;
+        if(svc != null && _isBound) {
+            setPref(PREF_AUTO_START, true);
+            svc.manualStart();
         } else {
-            // network but no router context
-            tv.setText("Router version: " + _ourVersion + "\n");
-            //tv.setVisibility(View.INVISIBLE);
-            /**
-             * **
-             * RouterService svc = _routerService; String status = "connected? "
-             * + Util.isConnected(this) + "\nMemory: " +
-             * DataHelper.formatSize(Runtime.getRuntime().totalMemory() -
-             * Runtime.getRuntime().freeMemory()) + "B / " +
-             * DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B' +
-             * "\nhave ctx? " + (ctx != null) + "\nhave svc? " + (svc != null) +
-             * "\nis bound? " + _isBound + "\nsvc state: " + (svc == null ?
-             * "null" : svc.getState()) + "\ncan start? " + (svc == null ?
-             * "null" : svc.canManualStart()) + "\ncan stop? " + (svc == null ?
-             * "null" : svc.canManualStop()); tv.setText(status);
-             * tv.setVisibility(View.VISIBLE);
-          ***
-             */
+            (new File(_myDir, "wrapper.log")).delete();
+            startRouter();
         }
     }
 
-    private void checkDialog() {
-        String oldVersion = getPref(PREF_INSTALLED_VERSION, "??");
-        if(oldVersion.equals("??")) {
-            showDialog(DIALOG_NEW_INSTALL);
-        } else {
-            String currentVersion = Util.getOurVersion(this);
-            if(!oldVersion.equals(currentVersion)) {
-                showDialog(DIALOG_NEW_VERSION);
-            }
+    public boolean onStopRouterClicked() {
+        RouterService svc = _routerService;
+        if(svc != null && _isBound) {
+            setPref(PREF_AUTO_START, false);
+            svc.manualQuit();
+            return true;
         }
+        return false;
     }
 
-    @Override
-    protected Dialog onCreateDialog(int id) {
-        final String currentVersion = Util.getOurVersion(this);
-        Dialog rv = null;
-        AlertDialog.Builder b = new AlertDialog.Builder(this);
-        switch(id) {
-            case DIALOG_NEW_INSTALL:
-                b.setMessage(getResources().getText(R.string.welcome_new_install)).setCancelable(false).setPositiveButton("OK", new DialogInterface.OnClickListener() {
-
-                    public void onClick(DialogInterface dialog, int id) {
-                        setPref(PREF_INSTALLED_VERSION, currentVersion);
-                        dialog.cancel();
-                        MainActivity.this.removeDialog(id);
-                    }
-                }).setNeutralButton("Release Notes", new DialogInterface.OnClickListener() {
-
-                    public void onClick(DialogInterface dialog, int id) {
-                        setPref(PREF_INSTALLED_VERSION, currentVersion);
-                        dialog.cancel();
-                        MainActivity.this.removeDialog(id);
-                        Intent intent = new Intent(MainActivity.this, TextResourceActivity.class);
-                        intent.putExtra(TextResourceActivity.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
-                        startActivity(intent);
-                    }
-                }).setNegativeButton("Licenses", new DialogInterface.OnClickListener() {
+    // VersionDialog.VersionDialogListener
 
-                    public void onClick(DialogInterface dialog, int id) {
-                        setPref(PREF_INSTALLED_VERSION, currentVersion);
-                        dialog.cancel();
-                        MainActivity.this.removeDialog(id);
-                        Intent intent = new Intent(MainActivity.this, LicenseActivity.class);
-                        startActivity(intent);
-                    }
-                });
-                rv = b.create();
-                break;
-
-            case DIALOG_NEW_VERSION:
-                b.setMessage(getResources().getText(R.string.welcome_new_version) + " " + currentVersion).setCancelable(true).setPositiveButton("OK", new DialogInterface.OnClickListener() {
-
-                    public void onClick(DialogInterface dialog, int id) {
-                        setPref(PREF_INSTALLED_VERSION, currentVersion);
-                        try {
-                            dialog.dismiss();
-                        } catch(Exception e) {
-                        }
-                        MainActivity.this.removeDialog(id);
-                    }
-                }).setNegativeButton("Release Notes", new DialogInterface.OnClickListener() {
-
-                    public void onClick(DialogInterface dialog, int id) {
-                        setPref(PREF_INSTALLED_VERSION, currentVersion);
-                        try {
-                            dialog.dismiss();
-                        } catch(Exception e) {
-                        }
-                        MainActivity.this.removeDialog(id);
-                        Intent intent = new Intent(MainActivity.this, TextResourceActivity.class);
-                        intent.putExtra(TextResourceActivity.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
-                        startActivity(intent);
-                    }
-                });
-
-                rv = b.create();
-                break;
-        }
-        return rv;
+    public void onFirstRun() {
+        mDrawerLayout.openDrawer(mDrawerList);
     }
 }
diff --git a/src/net/i2p/android/router/activity/PeersActivity.java b/src/net/i2p/android/router/activity/PeersActivity.java
index 395ec02802a33f415eacc39fcd88f4eee74f5f48..321eb806b31a1bcbce979e7ba731c1dd97a23197 100644
--- a/src/net/i2p/android/router/activity/PeersActivity.java
+++ b/src/net/i2p/android/router/activity/PeersActivity.java
@@ -1,42 +1,22 @@
 package net.i2p.android.router.activity;
 
-import android.os.Bundle;
-import android.view.KeyEvent;
-import android.view.MenuItem;
-import android.webkit.WebView;
-import java.io.IOException;
-import java.io.StringWriter;
 import net.i2p.android.router.R;
+import net.i2p.android.router.fragment.PeersFragment;
 import net.i2p.android.router.service.RouterService;
-import net.i2p.android.router.util.Util;
-import net.i2p.router.CommSystemFacade;
+import android.os.Bundle;
 
 public class PeersActivity extends I2PActivityBase {
-
-    private I2PWebViewClient _wvClient;
-
-    // TODO add some inline style
-    private static final String HEADER = "<html><head></head><body>";
-    private static final String FOOTER = "</body></html>";
-
     @Override
-    public void onCreate(Bundle savedInstanceState)
-    {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.peers);
-        WebView wv = (WebView) findViewById(R.id.peers_webview);
-        wv.getSettings().setLoadsImagesAutomatically(true); // was false
-        // http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
-        wv.getSettings().setUseWideViewPort(true);
-        _wvClient = new I2PWebViewClient(this);
-        wv.setWebViewClient(_wvClient);
-        wv.getSettings().setBuiltInZoomControls(true);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        update();
+        mDrawerToggle.setDrawerIndicatorEnabled(false);
+        // Start with the base view
+        if (savedInstanceState == null) {
+            PeersFragment f = new PeersFragment();
+            f.setArguments(getIntent().getExtras());
+            getSupportFragmentManager().beginTransaction()
+                    .add(R.id.main_fragment, f).commit();
+        }
     }
 
     /**
@@ -45,64 +25,7 @@ public class PeersActivity extends I2PActivityBase {
      */
     @Override
     protected void onRouterBind(RouterService svc) {
-        update();
-    }
-
-    private void update() {
-        WebView wv = (WebView) findViewById(R.id.peers_webview);
-        wv.clearHistory(); // fixes having to hit back.
-        CommSystemFacade comm = getCommSystem();
-        String data;
-        if (comm != null) {
-            StringWriter out = new StringWriter(32*1024);
-            out.append(HEADER);
-            try {
-                comm.renderStatusHTML(out, "http://thiswontwork.i2p/peers", 0);
-                out.append(FOOTER);
-                data = out.toString().replaceAll("/themes", "themes");
-            } catch (IOException ioe) {
-                data = HEADER + "Error: " + ioe + FOOTER;
-            }
-        } else {
-            data = HEADER + "No peer data available. The router is not running." + FOOTER;
-        }
-        try {
-            // wv.loadData(data, "text/html", "UTF-8");
-            // figure out a way to get /themes/console/images/outbound.png to load
-            // String url = "file://" + _myDir + "/docs/";
-            String url = "file:///android_asset/";
-            wv.loadDataWithBaseURL(url, data, "text/html", "UTF-8", url);
-        } catch (Exception e) {
-        }
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        WebView wv = (WebView) findViewById(R.id.peers_webview);
-        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
-            _wvClient.cancelAll();
-            wv.stopLoading();
-
-            // We do not want to go back, or keep history... Theere is no need to.
-            // What we DO want to do is exit!
-            //if (wv.canGoBack()) {
-            //    wv.goBack();
-            //    return true;
-            //}
-        }
-        return super.onKeyDown(keyCode, event);
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        WebView wv = (WebView) findViewById(R.id.peers_webview);
-        switch (item.getItemId()) {
-        case R.id.menu_reload:
-            update();
-            return true;
-
-        default:
-            return super.onOptionsItemSelected(item);
-        }
+        PeersFragment f = (PeersFragment) getSupportFragmentManager().findFragmentById(R.id.main_fragment);
+        f.update();
     }
 }
diff --git a/src/net/i2p/android/router/fragment/AddressbookFragment.java b/src/net/i2p/android/router/fragment/AddressbookFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f38a813524ca43ecaa44119b43d746d7450ee55
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/AddressbookFragment.java
@@ -0,0 +1,131 @@
+package net.i2p.android.router.fragment;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import net.i2p.I2PAppContext;
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.AddressbookSettingsActivity;
+import net.i2p.android.router.activity.HelpActivity;
+import net.i2p.client.naming.NamingService;
+
+public class AddressbookFragment extends ListFragment {
+    OnAddressSelectedListener mCallback;
+    private ArrayAdapter<String> mAdapter;
+
+    // Container Activity must implement this interface
+    public interface OnAddressSelectedListener {
+        public void onAddressSelected(CharSequence host);
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // This makes sure that the container activity has implemented
+        // the callback interface. If not, it throws an exception
+        try {
+            mCallback = (OnAddressSelectedListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement OnAddressSelectedListener");
+        }
+
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // Grab context if router has started, otherwise create new
+        // FIXME dup contexts, locking, ...
+        I2PAppContext ctx = I2PAppContext.getCurrentContext();
+        if (ctx == null) {
+            Properties props = new Properties();
+            String myDir = getActivity().getFilesDir().getAbsolutePath();
+            props.setProperty("i2p.dir.base", myDir);
+            props.setProperty("i2p.dir.config", myDir);
+            ctx = new I2PAppContext(props);
+        }
+
+        // get the names
+        NamingService ns = ctx.namingService();
+        // After router shutdown we get nothing... why?
+        Set<String> names = ns.getNames();
+
+        // set the empty text
+        setEmptyText("No hosts in address book, or your router is not up.");
+
+        // set the list
+        List<String> nameList = new ArrayList<String>(names);
+        Collections.sort(nameList);
+        mAdapter = new ArrayAdapter<String>(getActivity(), R.layout.addressbook_list_item, nameList);
+        setListAdapter(mAdapter);
+
+        // Show Toast with addressbook size
+        int sz = names.size();
+        Context context = getActivity().getApplicationContext();
+        CharSequence text = sz + " hosts in address book.";
+        if (sz == 1)
+            text = "1 host in address book.";
+        Toast.makeText(context, text, Toast.LENGTH_LONG).show();
+    }
+
+    @Override
+    public void onListItemClick(ListView parent, View view, int pos, long id) {
+        CharSequence host = ((TextView) view).getText();
+        mCallback.onAddressSelected(host);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+    	inflater.inflate(R.menu.fragment_addressbook_actions, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle presses on the action bar items
+        
+        switch (item.getItemId()) {
+            //case R.id.action_add_to_addressbook:
+            //    return true;
+        case R.id.action_addressbook_settings:
+            Intent si = new Intent(getActivity(), AddressbookSettingsActivity.class);
+            startActivity(si);
+            return true;
+        case R.id.action_addressbook_help:
+            Intent hi = new Intent(getActivity(), HelpActivity.class);
+            hi.putExtra(HelpActivity.REFERRER, "addressbook");
+            startActivity(hi);
+            return true;
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+    }
+
+    public void filterAddresses(String query) {
+        mAdapter.getFilter().filter(query);
+    }
+}
diff --git a/src/net/i2p/android/router/fragment/I2PFragmentBase.java b/src/net/i2p/android/router/fragment/I2PFragmentBase.java
new file mode 100644
index 0000000000000000000000000000000000000000..c54b1719aeebdc20094a92a560effb1e89aa0db5
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/I2PFragmentBase.java
@@ -0,0 +1,91 @@
+package net.i2p.android.router.fragment;
+
+import net.i2p.router.CommSystemFacade;
+import net.i2p.router.NetworkDatabaseFacade;
+import net.i2p.router.Router;
+import net.i2p.router.RouterContext;
+import net.i2p.router.TunnelManagerFacade;
+import net.i2p.router.peermanager.ProfileOrganizer;
+import net.i2p.router.transport.FIFOBandwidthLimiter;
+import net.i2p.stat.StatManager;
+import android.app.Activity;
+import android.support.v4.app.Fragment;
+
+public class I2PFragmentBase extends Fragment {
+    RouterContextProvider mCallback;
+
+    protected static final String PREF_INSTALLED_VERSION = "app.version";
+
+    // Container Activity must implement this interface
+    public interface RouterContextProvider {
+        public RouterContext getRouterContext();
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // This makes sure that the container activity has implemented
+        // the callback interface. If not, it throws an exception
+        try {
+            mCallback = (RouterContextProvider) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement RouterContextProvider");
+        }
+
+    }
+
+    protected RouterContext getRouterContext() {
+        return mCallback.getRouterContext();
+    }
+
+    protected Router getRouter() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.router();
+    }
+
+    protected NetworkDatabaseFacade getNetDb() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.netDb();
+    }
+
+    protected ProfileOrganizer getProfileOrganizer() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.profileOrganizer();
+    }
+
+    protected TunnelManagerFacade getTunnelManager() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.tunnelManager();
+    }
+
+    protected CommSystemFacade getCommSystem() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.commSystem();
+    }
+
+    protected FIFOBandwidthLimiter getBandwidthLimiter() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.bandwidthLimiter();
+    }
+
+    protected StatManager getStatManager() {
+        RouterContext ctx = getRouterContext();
+        if (ctx == null)
+            return null;
+        return ctx.statManager();
+    }
+}
diff --git a/src/net/i2p/android/router/activity/I2PWebViewClient.java b/src/net/i2p/android/router/fragment/I2PWebViewClient.java
similarity index 99%
rename from src/net/i2p/android/router/activity/I2PWebViewClient.java
rename to src/net/i2p/android/router/fragment/I2PWebViewClient.java
index 375bb1478dd570a467dda1b5fae46535d272a816..9b0ee06f64b08a2b695b8c54793873d3a2206b7a 100644
--- a/src/net/i2p/android/router/activity/I2PWebViewClient.java
+++ b/src/net/i2p/android/router/fragment/I2PWebViewClient.java
@@ -1,7 +1,6 @@
-package net.i2p.android.router.activity;
+package net.i2p.android.router.fragment;
 
 import android.app.ProgressDialog;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.graphics.Bitmap;
 import android.net.Uri;
@@ -38,10 +37,6 @@ class I2PWebViewClient extends WebViewClient {
     private static final String ERROR_URL = "<p>Unable to load URL: ";
     private static final String ERROR_ROUTER = "<p>Your router (or the HTTP proxy) does not appear to be running.</p>";
 
-    public I2PWebViewClient(Context ctx) {
-        super();
-    }
-
     @Override
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
         Util.d("Should override? " + url);
diff --git a/src/net/i2p/android/router/fragment/LicenseFragment.java b/src/net/i2p/android/router/fragment/LicenseFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e51d3010d26f587612a2eb3f00fb988a17e1a33
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/LicenseFragment.java
@@ -0,0 +1,42 @@
+package net.i2p.android.router.fragment;
+
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import net.i2p.android.router.R;
+
+public class LicenseFragment extends ListFragment {
+
+    private static final String[] names = {
+        "Android Application License", "Apache 2.0",
+        "Router License Overview", "Blockfile", "Crypto Filters", "ElGamal / DSA",
+        "GPLv2", "LGPLv2.1", "GPLv3", "LGPLv3", "FatCowIcons",
+        "Ministreaming",
+        "InstallCert", "SHA-256", "SNTP", "Addressbook"};
+
+    private static final int[] files = {
+        R.raw.license_app_txt, R.raw.license_apache20_txt,
+        R.raw.licenses_txt, R.raw.license_blockfile_txt, R.raw.license_bsd_txt, R.raw.license_elgamaldsa_txt,
+        R.raw.license_gplv2_txt, R.raw.license_lgplv2_1_txt, R.raw.license_gplv3_txt, R.raw.license_lgplv3_txt,
+        R.raw.license_fatcowicons_txt, R.raw.license_bsd_txt,
+        R.raw.license_installcert_txt, R.raw.license_sha256_txt, R.raw.license_sntp_txt, R.raw.license_addressbook_txt};
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        setListAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, names));
+    }
+
+    @Override
+    public void onListItemClick(ListView parent, View view, int pos, long id) {
+        TextResourceDialog dialog = new TextResourceDialog();
+        Bundle args = new Bundle();
+        args.putString(TextResourceDialog.TEXT_DIALOG_TITLE, names[pos]);
+        args.putInt(TextResourceDialog.TEXT_RESOURCE_ID, files[pos]);
+        dialog.setArguments(args);
+        dialog.show(getActivity().getSupportFragmentManager(), "license");
+    }
+}
diff --git a/src/net/i2p/android/router/fragment/MainFragment.java b/src/net/i2p/android/router/fragment/MainFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..5ff43d499dde06be6d98b5caf4b9c8244da51c4a
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/MainFragment.java
@@ -0,0 +1,412 @@
+package net.i2p.android.router.fragment;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import java.text.DecimalFormat;
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.I2PActivityBase;
+import net.i2p.android.router.util.Util;
+import net.i2p.data.DataHelper;
+import net.i2p.router.RouterContext;
+
+public class MainFragment extends I2PFragmentBase {
+
+    private Handler _handler;
+    private Runnable _updater;
+    private Runnable _oneShotUpdate;
+    private String _savedStatus;
+    private String _ourVersion;
+    private boolean _keep = true;
+    private boolean _startPressed = false;
+    protected static final String PROP_NEW_INSTALL = "i2p.newInstall";
+    protected static final String PROP_NEW_VERSION = "i2p.newVersion";
+    RouterControlListener mCallback;
+
+    // Container Activity must implement this interface
+    public interface RouterControlListener {
+        public boolean shouldShowStart();
+        public boolean shouldShowStop();
+        public void onStartRouterClicked();
+        public boolean onStopRouterClicked();
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // This makes sure that the container activity has implemented
+        // the callback interface. If not, it throws an exception
+        try {
+            mCallback = (RouterControlListener) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement RouterControlListener");
+        }
+
+    }
+
+    /**
+     * Called when the fragment is first created.
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // Init stuff here so settings work.
+        if(savedInstanceState != null) {
+            String saved = savedInstanceState.getString("status");
+            if(saved != null) {
+                _savedStatus = saved;
+            }
+        }
+        _ourVersion = Util.getOurVersion(getActivity());
+
+        _keep = true;
+
+        _handler = new Handler();
+        _updater = new Updater();
+        _oneShotUpdate = new OneShotUpdate();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.fragment_main, container, false);
+
+        Button b = (Button) v.findViewById(R.id.news_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                getActivity().getSupportFragmentManager()
+                             .beginTransaction()
+                             .replace(R.id.main_fragment, new NewsFragment())
+                             .addToBackStack(null)
+                             .commit();
+            }
+        });
+
+        b = (Button) v.findViewById(R.id.website_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                //intent.setData((new Uri.Builder()).scheme("http").authority("www.i2p2.de").path("/").build());
+                WebFragment f = new WebFragment();
+                Bundle args = new Bundle();
+                args.putString(WebFragment.HTML_URI, "http://www.i2p2.de/");
+                f.setArguments(args);
+                getActivity().getSupportFragmentManager()
+                             .beginTransaction()
+                             .replace(R.id.main_fragment, f)
+                             .addToBackStack(null)
+                             .commit();
+            }
+        });
+
+        b = (Button) v.findViewById(R.id.faq_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                //intent.setData((new Uri.Builder()).scheme("http").authority("www.i2p2.de").path("/faq").build());
+                WebFragment f = new WebFragment();
+                Bundle args = new Bundle();
+                args.putString(WebFragment.HTML_URI, "http://www.i2p2.de/faq");
+                f.setArguments(args);
+                getActivity().getSupportFragmentManager()
+                             .beginTransaction()
+                             .replace(R.id.main_fragment, f)
+                             .addToBackStack(null)
+                             .commit();
+            }
+        });
+
+        b = (Button) v.findViewById(R.id.welcome_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                WebFragment f = new WebFragment();
+                Bundle args = new Bundle();
+                args.putInt(WebFragment.HTML_RESOURCE_ID, R.raw.welcome_html);
+                f.setArguments(args);
+                getActivity().getSupportFragmentManager()
+                             .beginTransaction()
+                             .replace(R.id.main_fragment, f)
+                             .addToBackStack(null)
+                             .commit();
+            }
+        });
+
+        /*
+         * hidden, unused b = (Button) v.findViewById(R.id.router_stop_button);
+         * b.setOnClickListener(new View.OnClickListener() { public void
+         * onClick(View view) { RouterService svc = _routerService; if (svc !=
+         * null && _isBound) { setPref(PREF_AUTO_START, false);
+         * svc.manualStop(); updateOneShot(); } } });
+         */
+
+        b = (Button) v.findViewById(R.id.router_start_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                _startPressed = true;
+                mCallback.onStartRouterClicked();
+                updateOneShot();
+            }
+        });
+
+        b = (Button) v.findViewById(R.id.router_quit_button);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                if(mCallback.onStopRouterClicked()) {
+                    updateOneShot();
+                }
+            }
+        });
+
+        return v;
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        _handler.removeCallbacks(_updater);
+        _handler.removeCallbacks(_oneShotUpdate);
+        if(_savedStatus != null) {
+            TextView tv = (TextView) getActivity().findViewById(R.id.main_status_text);
+            tv.setText(_savedStatus);
+        }
+        checkDialog();
+        _handler.postDelayed(_updater, 100);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        _handler.removeCallbacks(_updater);
+        _handler.removeCallbacks(_oneShotUpdate);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateOneShot();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        if(_savedStatus != null) {
+            outState.putString("status", _savedStatus);
+        }
+        super.onSaveInstanceState(outState);
+    }
+
+    private void updateOneShot() {
+        _handler.postDelayed(_oneShotUpdate, 100);
+    }
+
+    private class OneShotUpdate implements Runnable {
+
+        public void run() {
+            updateVisibility();
+            updateStatus();
+        }
+    }
+
+    private class Updater implements Runnable {
+
+        private int counter;
+        private final int delay = 1000;
+        private final int toloop = delay / 500;
+        public void run() {
+            updateVisibility();
+            if(counter++ % toloop == 0) {
+                updateStatus();
+            }
+            //_handler.postDelayed(this, 2500);
+            _handler.postDelayed(this, delay);
+        }
+    }
+
+    private void updateVisibility() {
+        boolean showStart = mCallback.shouldShowStart();
+        Button start = (Button) getActivity().findViewById(R.id.router_start_button);
+        start.setVisibility(showStart ? View.VISIBLE : View.INVISIBLE);
+
+        boolean showStop = mCallback.shouldShowStop();
+        // Old stop but leave in memory. Always hide for now.
+        // Button stop = (Button) findViewById(R.id.router_stop_button);
+        // stop.setVisibility( /* showStop ? View.VISIBLE : */ View.INVISIBLE);
+
+        Button quit = (Button) getActivity().findViewById(R.id.router_quit_button);
+        quit.setVisibility(showStop ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public boolean onBackPressed() {
+        RouterContext ctx = getRouterContext();
+        // RouterService svc = _routerService; Which is better to use?!
+        _keep = Util.isConnected(getActivity()) && (ctx != null || _startPressed);
+        Util.d("*********************************************************");
+        Util.d("Back pressed, Keep? " + _keep);
+        Util.d("*********************************************************");
+        return false;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if(!_keep) {
+            Thread t = new Thread(new KillMe());
+            t.start();
+        }
+    }
+
+    private class KillMe implements Runnable {
+
+        public void run() {
+            Util.d("*********************************************************");
+            Util.d("KillMe started!");
+            Util.d("*********************************************************");
+            try {
+                Thread.sleep(500); // is 500ms long enough?
+            } catch(InterruptedException ex) {
+            }
+            System.exit(0);
+        }
+    }
+
+    private void updateStatus() {
+        RouterContext ctx = getRouterContext();
+        TextView tv = (TextView) getActivity().findViewById(R.id.main_status_text);
+
+        if(!Util.isConnected(getActivity())) {
+            tv.setText("Router version: " + _ourVersion + "\nNo Internet connection is available");
+            tv.setVisibility(View.VISIBLE);
+        } else if(ctx != null) {
+            if(_startPressed) {
+                _startPressed = false;
+            }
+            short reach = ctx.commSystem().getReachabilityStatus();
+            int active = ctx.commSystem().countActivePeers();
+            int known = Math.max(ctx.netDb().getKnownRouters() - 1, 0);
+            int inEx = ctx.tunnelManager().getFreeTunnelCount();
+            int outEx = ctx.tunnelManager().getOutboundTunnelCount();
+            int inCl = ctx.tunnelManager().getInboundClientTunnelCount();
+            int outCl = ctx.tunnelManager().getOutboundClientTunnelCount();
+            int part = ctx.tunnelManager().getParticipatingCount();
+            double dLag = ctx.statManager().getRate("jobQueue.jobLag").getRate(60000).getAverageValue();
+            String jobLag = DataHelper.formatDuration((long) dLag);
+            String msgDelay = DataHelper.formatDuration(ctx.throttle().getMessageDelay());
+            String uptime = DataHelper.formatDuration(ctx.router().getUptime());
+
+            String netstatus = "Unknown";
+            if(reach == net.i2p.router.CommSystemFacade.STATUS_DIFFERENT) {
+                netstatus = "Different";
+            }
+            if(reach == net.i2p.router.CommSystemFacade.STATUS_HOSED) {
+                netstatus = "Hosed";
+            }
+            if(reach == net.i2p.router.CommSystemFacade.STATUS_OK) {
+                netstatus = "OK";
+            }
+            if(reach == net.i2p.router.CommSystemFacade.STATUS_REJECT_UNSOLICITED) {
+                netstatus = "Reject Unsolicited";
+            }
+            String tunnelStatus = ctx.throttle().getTunnelStatus();
+            //ctx.commSystem().getReachabilityStatus();
+            double inBW = ctx.bandwidthLimiter().getReceiveBps() / 1024;
+            double outBW = ctx.bandwidthLimiter().getSendBps() / 1024;
+
+            // control total width
+            DecimalFormat fmt;
+            if(inBW >= 1000 || outBW >= 1000) {
+                fmt = new DecimalFormat("#0");
+            } else if(inBW >= 100 || outBW >= 100) {
+                fmt = new DecimalFormat("#0.0");
+            } else {
+                fmt = new DecimalFormat("#0.00");
+            }
+
+            double kBytesIn = ctx.bandwidthLimiter().getTotalAllocatedInboundBytes() / 1024;
+            double kBytesOut = ctx.bandwidthLimiter().getTotalAllocatedOutboundBytes() / 1024;
+
+            // control total width
+            DecimalFormat kBfmt;
+            if(kBytesIn >= 1000 || kBytesOut >= 1000) {
+                kBfmt = new DecimalFormat("#0");
+            } else if(kBytesIn >= 100 || kBytesOut >= 100) {
+                kBfmt = new DecimalFormat("#0.0");
+            } else {
+                kBfmt = new DecimalFormat("#0.00");
+            }
+
+            String status =
+                    "ROUTER STATUS"
+                    + "\nNetwork: " + netstatus
+                    + "\nPeers active/known: " + active + " / " + known
+                    + "\nExploratory Tunnels in/out: " + inEx + " / " + outEx
+                    + "\nClient Tunnels in/out: " + inCl + " / " + outCl;
+
+
+            // Need to see if we have the participation option set to on.
+            // I thought there was a router method for that? I guess not! WHY NOT?
+            // It would be easier if we had a number to test status.
+            String participate = "\nParticipation: " + tunnelStatus +" (" + part + ")";
+
+            String details =
+                    "\nBandwidth in/out: " + fmt.format(inBW) + " / " + fmt.format(outBW) + " KBps"
+                    + "\nData usage in/out: " + kBfmt.format(kBytesIn) + " / " + kBfmt.format(kBytesOut) + " KB"
+                    + "\nMemory: " + DataHelper.formatSize(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())
+                    + "B / " + DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B'
+                    + "\nJob Lag: " + jobLag
+                    + "\nMsg Delay: " + msgDelay
+                    + "\nUptime: " + uptime;
+
+            _savedStatus = "Router version: " + _ourVersion + "\n" + status + participate + details;
+            tv.setText(_savedStatus);
+            tv.setVisibility(View.VISIBLE);
+        } else {
+            // network but no router context
+            tv.setText("Router version: " + _ourVersion + "\n");
+            //tv.setVisibility(View.INVISIBLE);
+            /**
+             * **
+             * RouterService svc = _routerService; String status = "connected? "
+             * + Util.isConnected(this) + "\nMemory: " +
+             * DataHelper.formatSize(Runtime.getRuntime().totalMemory() -
+             * Runtime.getRuntime().freeMemory()) + "B / " +
+             * DataHelper.formatSize(Runtime.getRuntime().maxMemory()) + 'B' +
+             * "\nhave ctx? " + (ctx != null) + "\nhave svc? " + (svc != null) +
+             * "\nis bound? " + _isBound + "\nsvc state: " + (svc == null ?
+             * "null" : svc.getState()) + "\ncan start? " + (svc == null ?
+             * "null" : svc.canManualStart()) + "\ncan stop? " + (svc == null ?
+             * "null" : svc.canManualStop()); tv.setText(status);
+             * tv.setVisibility(View.VISIBLE);
+          ***
+             */
+        }
+    }
+
+    private void checkDialog() {
+        VersionDialog dialog = new VersionDialog();
+        String oldVersion = ((I2PActivityBase) getActivity()).getPref(PREF_INSTALLED_VERSION, "??");
+        if(oldVersion.equals("??")) {
+            Bundle args = new Bundle();
+            args.putInt(VersionDialog.DIALOG_TYPE, VersionDialog.DIALOG_NEW_INSTALL);
+            dialog.setArguments(args);
+            dialog.show(getActivity().getSupportFragmentManager(), "newinstall");
+        } else {
+            String currentVersion = Util.getOurVersion(getActivity());
+            if(!oldVersion.equals(currentVersion)) {
+                Bundle args = new Bundle();
+                args.putInt(VersionDialog.DIALOG_TYPE, VersionDialog.DIALOG_NEW_VERSION);
+                dialog.setArguments(args);
+                dialog.show(getActivity().getSupportFragmentManager(), "newversion");
+            }
+        }
+    }
+}
diff --git a/src/net/i2p/android/router/activity/NewsActivity.java b/src/net/i2p/android/router/fragment/NewsFragment.java
similarity index 71%
rename from src/net/i2p/android/router/activity/NewsActivity.java
rename to src/net/i2p/android/router/fragment/NewsFragment.java
index efe1ffdd5f32d12cb1e94154055d0db26e273243..9b988d03cef63f6b594d2ae942b668258be87874 100644
--- a/src/net/i2p/android/router/activity/NewsActivity.java
+++ b/src/net/i2p/android/router/fragment/NewsFragment.java
@@ -1,8 +1,10 @@
-package net.i2p.android.router.activity;
+package net.i2p.android.router.fragment;
 
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 import android.webkit.WebView;
 import android.widget.TextView;
 import java.io.ByteArrayOutputStream;
@@ -14,7 +16,7 @@ import java.io.UnsupportedEncodingException;
 import net.i2p.android.apps.NewsFetcher;
 import net.i2p.android.router.R;
 
-public class NewsActivity extends I2PActivityBase {
+public class NewsFragment extends I2PFragmentBase {
 
     private I2PWebViewClient _wvClient;
     private long _lastChanged;
@@ -29,17 +31,18 @@ public class NewsActivity extends I2PActivityBase {
     private static final String FOOTER = "</body></html>";
 
     @Override
-    public void onCreate(Bundle savedInstanceState)
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState)
     {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.news);
-        WebView wv = (WebView) findViewById(R.id.news_webview);
+        View v = inflater.inflate(R.layout.news, container, false);
+        WebView wv = (WebView) v.findViewById(R.id.news_webview);
         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(this);
+        _wvClient = new I2PWebViewClient();
         wv.setWebViewClient(_wvClient);
         wv.getSettings().setBuiltInZoomControls(true);
+        return v;
     }
 
     @Override
@@ -49,18 +52,19 @@ public class NewsActivity extends I2PActivityBase {
         NewsFetcher nf = NewsFetcher.getInstance();
         if (nf != null) {
             // always update the text
-            TextView tv = (TextView) findViewById(R.id.news_status);
+            TextView tv = (TextView) getActivity().findViewById(R.id.news_status);
             tv.setText(WARNING + nf.status().replace("&nbsp;", " "));
         }
 
         // only update the webview if we need to
-        File newsFile = new File(_myDir, "docs/news.xml");
+        // XXX Gets dir directly instead of the one stored in the Activity (for now)
+        File newsFile = new File(getActivity().getFilesDir().getAbsolutePath(), "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);
+        WebView wv = (WebView) getActivity().findViewById(R.id.news_webview);
 
         InputStream in = null;
         ByteArrayOutputStream out = new ByteArrayOutputStream(2048);
@@ -93,17 +97,14 @@ public class NewsActivity extends I2PActivityBase {
         }
     }
 
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        WebView wv = (WebView) findViewById(R.id.news_webview);
-        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
-            _wvClient.cancelAll();
-            wv.stopLoading();
-            if (wv.canGoBack()) {
-                wv.goBack();
-                return true;
-            }
+    public boolean onBackPressed() {
+        WebView wv = (WebView) getActivity().findViewById(R.id.news_webview);
+        _wvClient.cancelAll();
+        wv.stopLoading();
+        if (wv.canGoBack()) {
+            wv.goBack();
+            return true;
         }
-        return super.onKeyDown(keyCode, event);
+        return false;
     }
 }
diff --git a/src/net/i2p/android/router/fragment/PeersFragment.java b/src/net/i2p/android/router/fragment/PeersFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..58f0e07bc9e2343db0f811f0e1d87a8c4c086782
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/PeersFragment.java
@@ -0,0 +1,99 @@
+package net.i2p.android.router.fragment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import java.io.IOException;
+import java.io.StringWriter;
+import net.i2p.android.router.R;
+import net.i2p.router.CommSystemFacade;
+
+public class PeersFragment extends I2PFragmentBase {
+
+    private I2PWebViewClient _wvClient;
+
+    // TODO add some inline style
+    private static final String HEADER = "<html><head></head><body>";
+    private static final String FOOTER = "</body></html>";
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState)
+    {
+        View v = inflater.inflate(R.layout.peers, container, false);
+        WebView wv = (WebView) v.findViewById(R.id.peers_webview);
+        wv.getSettings().setLoadsImagesAutomatically(true); // was false
+        // http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
+        wv.getSettings().setUseWideViewPort(true);
+        _wvClient = new I2PWebViewClient();
+        wv.setWebViewClient(_wvClient);
+        wv.getSettings().setBuiltInZoomControls(true);
+        return v;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        update();
+    }
+
+    public void update() {
+        WebView wv = (WebView) getActivity().findViewById(R.id.peers_webview);
+        wv.clearHistory(); // fixes having to hit back.
+        CommSystemFacade comm = getCommSystem();
+        String data;
+        if (comm != null) {
+            StringWriter out = new StringWriter(32*1024);
+            out.append(HEADER);
+            try {
+                comm.renderStatusHTML(out, "http://thiswontwork.i2p/peers", 0);
+                out.append(FOOTER);
+                data = out.toString().replaceAll("/themes", "themes");
+            } catch (IOException ioe) {
+                data = HEADER + "Error: " + ioe + FOOTER;
+            }
+        } else {
+            data = HEADER + "No peer data available. The router is not running." + FOOTER;
+        }
+        try {
+            // wv.loadData(data, "text/html", "UTF-8");
+            // figure out a way to get /themes/console/images/outbound.png to load
+            // String url = "file://" + _myDir + "/docs/";
+            String url = "file:///android_asset/";
+            wv.loadDataWithBaseURL(url, data, "text/html", "UTF-8", url);
+        } catch (Exception e) {
+        }
+    }
+
+    public boolean onBackPressed() {
+        WebView wv = (WebView) getActivity().findViewById(R.id.peers_webview);
+        _wvClient.cancelAll();
+        wv.stopLoading();
+
+        // We do not want to go back, or keep history... There is no need to.
+        // What we DO want to do is exit!
+        //if (wv.canGoBack()) {
+        //    wv.goBack();
+        //    return true;
+        //}
+        return false;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        WebView wv = (WebView) getActivity().findViewById(R.id.peers_webview);
+        switch (item.getItemId()) {
+        /*
+        case R.id.menu_reload:
+            update();
+            return true;
+            */
+
+        default:
+            return super.onOptionsItemSelected(item);
+        }
+    }
+}
diff --git a/src/net/i2p/android/router/activity/TextResourceActivity.java b/src/net/i2p/android/router/fragment/TextResourceDialog.java
similarity index 63%
rename from src/net/i2p/android/router/activity/TextResourceActivity.java
rename to src/net/i2p/android/router/fragment/TextResourceDialog.java
index b9c2ed33d1d7050ee6177c727b7e8edd6fcd08d4..e4556dd3987fa58e12224ade8e763c1c9bcacddc 100644
--- a/src/net/i2p/android/router/activity/TextResourceActivity.java
+++ b/src/net/i2p/android/router/fragment/TextResourceDialog.java
@@ -1,9 +1,12 @@
-package net.i2p.android.router.activity;
+package net.i2p.android.router.fragment;
 
-import android.content.Intent;
 import android.content.res.Resources;
 import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
 import android.text.method.ScrollingMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
 import android.widget.TextView;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -16,24 +19,28 @@ import net.i2p.android.router.util.Util;
  *  Display a raw text resource.
  *  The resource ID must be passed as an extra in the intent.
  */
-public class TextResourceActivity extends I2PActivityBase {
+public class TextResourceDialog extends DialogFragment {
 
-    final static String TEXT_RESOURCE_ID = "text_resource_id";
+    public static final String TEXT_DIALOG_TITLE = "text_title";
+    public final static String TEXT_RESOURCE_ID = "text_resource_id";
 
     @Override
-    public void onCreate(Bundle savedInstanceState)
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState)
     {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.text_resource);
-        TextView tv = (TextView) findViewById(R.id.text_resource_text);
+        View v = inflater.inflate(R.layout.text_resource, container, false);
+        TextView tv = (TextView) v.findViewById(R.id.text_resource_text);
         tv.setMovementMethod(ScrollingMovementMethod.getInstance());
-        Intent intent = getIntent();
-        int id = intent.getIntExtra(TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
+        String title = getArguments().getString(TEXT_DIALOG_TITLE);
+        if (title != null)
+            getDialog().setTitle(title);
+        int id = getArguments().getInt(TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
         if (id == R.raw.releasenotes_txt)
-            tv.setText("Release Notes for Release " + Util.getOurVersion(this) + "\n\n" +
+            tv.setText("Release Notes for Release " + Util.getOurVersion(getActivity()) + "\n\n" +
                        getResourceAsString(id));
         else
             tv.setText(getResourceAsString(id));
+        return v;
     }
 
     private String getResourceAsString(int id) {
diff --git a/src/net/i2p/android/router/fragment/VersionDialog.java b/src/net/i2p/android/router/fragment/VersionDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..857eed20da188173d8ce06898ce9fbe52653f099
--- /dev/null
+++ b/src/net/i2p/android/router/fragment/VersionDialog.java
@@ -0,0 +1,127 @@
+package net.i2p.android.router.fragment;
+
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.I2PActivityBase;
+import net.i2p.android.router.activity.LicenseActivity;
+import net.i2p.android.router.util.Util;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+public class VersionDialog extends DialogFragment {
+    protected static final String DIALOG_TYPE = "dialog_type";
+    protected static final int DIALOG_NEW_INSTALL = 0;
+    protected static final int DIALOG_NEW_VERSION = 1;
+
+    public interface VersionDialogListener {
+        public void onFirstRun();
+    }
+
+    VersionDialogListener mListener;
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        // Verify that the host activity implements the callback interface
+        try {
+            // Instantiate the VersionDialogListener so we can tell the host
+            // if this is a new install.
+            mListener = (VersionDialogListener) activity;
+        } catch (ClassCastException e) {
+            // The activity doesn't implement the interface, throw exception
+            throw new ClassCastException(activity.toString()
+                    + " must implement VersionDialogListener");
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle SavedInstanceState) {
+        final String currentVersion = Util.getOurVersion(getActivity());
+        Dialog rv = null;
+        AlertDialog.Builder b = new AlertDialog.Builder(getActivity());
+        int id = getArguments().getInt(DIALOG_TYPE);
+        switch(id) {
+            case DIALOG_NEW_INSTALL:
+                b.setMessage(R.string.welcome_new_install)
+                 .setCancelable(false)
+                 .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+
+                    public void onClick(DialogInterface dialog, int id) {
+                        I2PActivityBase ab = (I2PActivityBase) getActivity();
+                        ab.setPref(MainFragment.PREF_INSTALLED_VERSION, currentVersion);
+                        dialog.cancel();
+                        mListener.onFirstRun();
+                    }
+                }).setNeutralButton(R.string.label_release_notes, new DialogInterface.OnClickListener() {
+
+                    public void onClick(DialogInterface dialog, int id) {
+                        I2PActivityBase ab = (I2PActivityBase) getActivity();
+                        ab.setPref(MainFragment.PREF_INSTALLED_VERSION, currentVersion);
+                        dialog.cancel();
+                        TextResourceDialog f = new TextResourceDialog();
+                        Bundle args = new Bundle();
+                        args.putInt(TextResourceDialog.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
+                        f.setArguments(args);
+                        getActivity().getSupportFragmentManager()
+                                     .beginTransaction()
+                                     .replace(R.id.main_fragment, f)
+                                     .addToBackStack(null)
+                                     .commit();
+                    }
+                }).setNegativeButton(R.string.label_licenses, new DialogInterface.OnClickListener() {
+
+                    public void onClick(DialogInterface dialog, int id) {
+                        I2PActivityBase ab = (I2PActivityBase) getActivity();
+                        ab.setPref(MainFragment.PREF_INSTALLED_VERSION, currentVersion);
+                        dialog.cancel();
+                        Intent intent = new Intent(getActivity(), LicenseActivity.class);
+                        startActivity(intent);
+                    }
+                });
+                rv = b.create();
+                break;
+
+            case DIALOG_NEW_VERSION:
+                b.setMessage(R.string.welcome_new_version + " " + currentVersion)
+                 .setCancelable(true)
+                 .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+
+                    public void onClick(DialogInterface dialog, int id) {
+                        I2PActivityBase ab = (I2PActivityBase) getActivity();
+                        ab.setPref(MainFragment.PREF_INSTALLED_VERSION, currentVersion);
+                        try {
+                            dialog.dismiss();
+                        } catch(Exception e) {
+                        }
+                    }
+                }).setNegativeButton(R.string.label_release_notes, new DialogInterface.OnClickListener() {
+
+                    public void onClick(DialogInterface dialog, int id) {
+                        I2PActivityBase ab = (I2PActivityBase) getActivity();
+                        ab.setPref(MainFragment.PREF_INSTALLED_VERSION, currentVersion);
+                        try {
+                            dialog.dismiss();
+                        } catch(Exception e) {
+                        }
+                        TextResourceDialog f = new TextResourceDialog();
+                        Bundle args = new Bundle();
+                        args.putInt(TextResourceDialog.TEXT_RESOURCE_ID, R.raw.releasenotes_txt);
+                        f.setArguments(args);
+                        getActivity().getSupportFragmentManager()
+                                     .beginTransaction()
+                                     .replace(R.id.main_fragment, f)
+                                     .addToBackStack(null)
+                                     .commit();
+                    }
+                });
+
+                rv = b.create();
+                break;
+        }
+        return rv;
+    }
+}
diff --git a/src/net/i2p/android/router/activity/WebActivity.java b/src/net/i2p/android/router/fragment/WebFragment.java
similarity index 69%
rename from src/net/i2p/android/router/activity/WebActivity.java
rename to src/net/i2p/android/router/fragment/WebFragment.java
index b1953ff1ffffdfa6c176544faa3417fac58a10b1..c42fb59690446b5334bd3c7e56dbd06245f56b74 100644
--- a/src/net/i2p/android/router/activity/WebActivity.java
+++ b/src/net/i2p/android/router/fragment/WebFragment.java
@@ -1,11 +1,12 @@
-package net.i2p.android.router.activity;
+package net.i2p.android.router.fragment;
 
-import android.content.Intent;
 import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Bundle;
-import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
 import android.webkit.WebView;
 import android.widget.TextView;
 import java.io.ByteArrayOutputStream;
@@ -14,42 +15,44 @@ import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import net.i2p.android.router.R;
 
-public class WebActivity extends I2PActivityBase {
+public class WebFragment extends I2PFragmentBase {
 
     private I2PWebViewClient _wvClient;
 
+    public final static String HTML_URI = "html_url";
     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 may not load images or CSS.";
 
     @Override
-    public void onCreate(Bundle savedInstanceState)
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState)
     {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.web);
-        TextView tv = (TextView) findViewById(R.id.browser_status);
+        View v = inflater.inflate(R.layout.web, container, false);
+        TextView tv = (TextView) v.findViewById(R.id.browser_status);
         tv.setText(WARNING);
-        WebView wv = (WebView) findViewById(R.id.browser_webview);
-        _wvClient = new I2PWebViewClient(this);
+        WebView wv = (WebView) v.findViewById(R.id.browser_webview);
+        _wvClient = new I2PWebViewClient();
         wv.setWebViewClient(_wvClient);
         wv.getSettings().setBuiltInZoomControls(true);
         // http://stackoverflow.com/questions/2369310/webview-double-tap-zoom-not-working-on-a-motorola-droid-a855
         wv.getSettings().setUseWideViewPort(true);
-        Intent intent = getIntent();
-        Uri uri = intent.getData();
-        if (uri != null) {
+        String uriStr =  getArguments().getString(HTML_URI);
+        if (uriStr != null) {
+            Uri uri = Uri.parse(uriStr);
             //wv.getSettings().setLoadsImagesAutomatically(true);
             //wv.loadUrl(uri.toString());
             // go thru the client so .i2p will work too
             _wvClient.shouldOverrideUrlLoading(wv, uri.toString());
         } else {
             wv.getSettings().setLoadsImagesAutomatically(false);
-            int id = intent.getIntExtra(HTML_RESOURCE_ID, 0);
+            int id = getArguments().getInt(HTML_RESOURCE_ID, 0);
             // no default, so restart should keep previous view
             if (id != 0)
                 loadResource(wv, id);
         }
+        return v;
     }
 
     private void loadResource(WebView wv, int id) {
@@ -76,26 +79,24 @@ public class WebActivity extends I2PActivityBase {
         }
     }
 
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        WebView wv = (WebView) findViewById(R.id.browser_webview);
-        if ((keyCode == KeyEvent.KEYCODE_BACK)) {
-            _wvClient.cancelAll();
-            wv.stopLoading();
-            if (wv.canGoBack()) {
-                // TODO go into history, get url and call shouldOverrideUrlLoading()
-                // so we have control ??? But then back won't work right
-                wv.goBack();
-                return true;
-            }
+    public boolean onBackPressed() {
+        WebView wv = (WebView) getActivity().findViewById(R.id.browser_webview);
+        _wvClient.cancelAll();
+        wv.stopLoading();
+        if (wv.canGoBack()) {
+            // TODO go into history, get url and call shouldOverrideUrlLoading()
+            // so we have control ??? But then back won't work right
+            wv.goBack();
+            return true;
         }
-        return super.onKeyDown(keyCode, event);
+        return false;
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        WebView wv = (WebView) findViewById(R.id.browser_webview);
+        WebView wv = (WebView) getActivity().findViewById(R.id.browser_webview);
         switch (item.getItemId()) {
+        /*
         case R.id.menu_reload:
             _wvClient.cancelAll();
             wv.stopLoading();
@@ -110,6 +111,7 @@ public class WebActivity extends I2PActivityBase {
                 _wvClient.shouldOverrideUrlLoading(wv, url);
             }
             return true;
+            */
 
         default:
             return super.onOptionsItemSelected(item);
diff --git a/src/net/i2p/android/wizard/model/AbstractWizardModel.java b/src/net/i2p/android/wizard/model/AbstractWizardModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..843b382e415a0a1feede54072d37af4c428ef74e
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/AbstractWizardModel.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a wizard model, including the pages/steps in the wizard, their dependencies, and their
+ * currently populated choices/values/selections.
+ *
+ * To create an actual wizard model, extend this class and implement {@link #onNewRootPageList()}.
+ */
+public abstract class AbstractWizardModel implements ModelCallbacks {
+    protected Context mContext;
+
+    private List<ModelCallbacks> mListeners = new ArrayList<ModelCallbacks>();
+    private PageList mRootPageList;
+
+    public AbstractWizardModel(Context context) {
+        mContext = context;
+        mRootPageList = onNewRootPageList();
+    }
+
+    /**
+     * Override this to define a new wizard model.
+     */
+    protected abstract PageList onNewRootPageList();
+
+    public void onPageDataChanged(Page page) {
+        // can't use for each because of concurrent modification (review fragment
+        // can get added or removed and will register itself as a listener)
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onPageDataChanged(page);
+        }
+    }
+
+    public void onPageTreeChanged() {
+        // can't use for each because of concurrent modification (review fragment
+        // can get added or removed and will register itself as a listener)
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onPageTreeChanged();
+        }
+    }
+
+    public Page findByKey(String key) {
+        return mRootPageList.findByKey(key);
+    }
+
+    public void load(Bundle savedValues) {
+        for (String key : savedValues.keySet()) {
+            mRootPageList.findByKey(key).resetData(savedValues.getBundle(key));
+        }
+    }
+
+    public void registerListener(ModelCallbacks listener) {
+        mListeners.add(listener);
+    }
+
+    public Bundle save() {
+        Bundle bundle = new Bundle();
+        for (Page page : getCurrentPageSequence()) {
+            bundle.putBundle(page.getKey(), page.getData());
+        }
+        return bundle;
+    }
+
+    /**
+     * Gets the current list of wizard steps, flattening nested (dependent) pages based on the
+     * user's choices.
+     */
+    public List<Page> getCurrentPageSequence() {
+        ArrayList<Page> flattened = new ArrayList<Page>();
+        mRootPageList.flattenCurrentPageSequence(flattened);
+        return flattened;
+    }
+
+    public void unregisterListener(ModelCallbacks listener) {
+        mListeners.remove(listener);
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/BranchPage.java b/src/net/i2p/android/wizard/model/BranchPage.java
new file mode 100644
index 0000000000000000000000000000000000000000..4feb56c9f84003fb1f9800d997e5ee80cd0e32a9
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/BranchPage.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import net.i2p.android.wizard.ui.SingleChoiceFragment;
+
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A page representing a branching point in the wizard. Depending on which choice is selected, the
+ * next set of steps in the wizard may change.
+ */
+public class BranchPage extends SingleFixedChoicePage {
+    private List<Branch> mBranches = new ArrayList<Branch>();
+
+    public BranchPage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Page findByKey(String key) {
+        if (getKey().equals(key)) {
+            return this;
+        }
+
+        for (Branch branch : mBranches) {
+            Page found = branch.childPageList.findByKey(key);
+            if (found != null) {
+                return found;
+            }
+        }
+
+        return null;
+    }
+
+    @Override
+    public void flattenCurrentPageSequence(ArrayList<Page> destination) {
+        super.flattenCurrentPageSequence(destination);
+        for (Branch branch : mBranches) {
+            if (branch.choice.equals(mData.getString(Page.SIMPLE_DATA_KEY))) {
+                branch.childPageList.flattenCurrentPageSequence(destination);
+                break;
+            }
+        }
+    }
+
+    public BranchPage addBranch(String choice, Page... childPages) {
+        PageList childPageList = new PageList(childPages);
+        for (Page page : childPageList) {
+            page.setParentKey(choice);
+        }
+        mBranches.add(new Branch(choice, childPageList));
+        return this;
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return SingleChoiceFragment.create(getKey());
+    }
+
+    public String getOptionAt(int position) {
+        return mBranches.get(position).choice;
+    }
+
+    public int getOptionCount() {
+        return mBranches.size();
+    }
+
+    @Override
+    public void getReviewItems(ArrayList<ReviewItem> dest) {
+        dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey()));
+    }
+
+    @Override
+    public boolean isCompleted() {
+        return !TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY));
+    }
+
+    @Override
+    public void notifyDataChanged() {
+        mCallbacks.onPageTreeChanged();
+        super.notifyDataChanged();
+    }
+
+    public BranchPage setValue(String value) {
+        mData.putString(SIMPLE_DATA_KEY, value);
+        return this;
+    }
+
+    private static class Branch {
+        public String choice;
+        public PageList childPageList;
+
+        private Branch(String choice, PageList childPageList) {
+            this.choice = choice;
+            this.childPageList = childPageList;
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/Conditional.java b/src/net/i2p/android/wizard/model/Conditional.java
new file mode 100644
index 0000000000000000000000000000000000000000..829ecf84e078d32160f76be8fb940ae061f9d51b
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/Conditional.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class Conditional implements ModelCallbacks {
+    private Object mData = null;
+    private List<Page> mConditionalPages = new ArrayList<Page>();
+
+    public void onPageDataChanged(Page page) {
+        mData = page.getData().get(Page.SIMPLE_DATA_KEY);
+        for (Page p : mConditionalPages)
+            p.isSatisfied();
+    }
+
+    public void onPageTreeChanged() {
+    }
+
+    public interface Condition {
+        public boolean isSatisfied();
+    }
+
+    public class EqualCondition<T> implements Condition {
+        private T mCompValue;
+
+        public EqualCondition(Page page, T compValue) {
+            mCompValue = compValue;
+            mConditionalPages.add(page);
+        }
+
+        public boolean isSatisfied() {
+            return mCompValue.equals(mData);
+        }
+    }
+
+    public class NotEqualCondition<T> implements Condition {
+        private T mCompValue;
+
+        public NotEqualCondition(Page page, T compValue) {
+            mCompValue = compValue;
+            mConditionalPages.add(page);
+        }
+
+        public boolean isSatisfied() {
+            return !(mCompValue.equals(mData));
+        }
+    }
+
+    public class EqualAnyCondition<T> implements Condition {
+        private ArrayList<T> mChoices = new ArrayList<T>();
+
+        public EqualAnyCondition(Page page, T... choices) {
+            mChoices.addAll(Arrays.asList(choices));
+            mConditionalPages.add(page);
+        }
+
+        public boolean isSatisfied() {
+            return mChoices.contains(mData);
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/I2PDestinationPage.java b/src/net/i2p/android/wizard/model/I2PDestinationPage.java
new file mode 100644
index 0000000000000000000000000000000000000000..31ce12312326a781d3e62e532d3991fd5abc6b21
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/I2PDestinationPage.java
@@ -0,0 +1,62 @@
+package net.i2p.android.wizard.model;
+
+import java.util.Locale;
+
+import android.support.v4.app.Fragment;
+
+import net.i2p.android.wizard.ui.I2PDestinationFragment;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.Destination;
+
+/**
+ * A page asking for an I2P Destination.
+ * This could be a B64, B32 or Addressbook domain.
+ */
+public class I2PDestinationPage extends SingleTextFieldPage {
+    private static final int BASE32_HASH_LENGTH = 52;   // 1 + Hash.HASH_LENGTH * 8 / 5
+    private String mFeedback;
+
+    public I2PDestinationPage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return I2PDestinationFragment.create(getKey());
+    }
+
+    @Override
+    public boolean isValid() {
+        String data = mData.getString(SIMPLE_DATA_KEY);
+        if (data.toLowerCase(Locale.US).endsWith(".b32.i2p")) { /* B32 */
+            if (data.length() != BASE32_HASH_LENGTH + 8) {
+                mFeedback = "Invalid B32";
+                return false;
+            }
+        } else if (data.endsWith(".i2p")) { /* Domain */
+            // Valid
+        } else if (data.length() >= 516) { /* B64 */
+            try {
+                new Destination().fromBase64(data);
+            } catch (DataFormatException dfe) {
+                mFeedback = "Invalid B64";
+                return false;
+            }
+        } else {
+            mFeedback = "Not a valid I2P Destination";
+            return false;
+        }
+        mFeedback = "";
+        return true;
+    }
+
+    @Override
+    public boolean showFeedback() {
+        return true;
+    }
+
+    @Override
+    public String getFeedback() {
+        return mFeedback;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/ModelCallbacks.java b/src/net/i2p/android/wizard/model/ModelCallbacks.java
new file mode 100644
index 0000000000000000000000000000000000000000..baddecf9b15e8e4a795a9bd2d2b88e8eb07c4c58
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/ModelCallbacks.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+/**
+ * Callback interface connecting {@link Page}, {@link AbstractWizardModel}, and model container
+ * objects (e.g. {@link net.i2p.android.i2ptunnel.activity.TunnelWizardActivity}.
+ */
+public interface ModelCallbacks {
+    void onPageDataChanged(Page page);
+    void onPageTreeChanged();
+}
diff --git a/src/net/i2p/android/wizard/model/MultipleFixedChoicePage.java b/src/net/i2p/android/wizard/model/MultipleFixedChoicePage.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ff5d748524219d22d4808b6027d25d4d5790951
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/MultipleFixedChoicePage.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import net.i2p.android.wizard.ui.MultipleChoiceFragment;
+
+import android.support.v4.app.Fragment;
+
+import java.util.ArrayList;
+
+/**
+ * A page offering the user a number of non-mutually exclusive choices.
+ */
+public class MultipleFixedChoicePage extends SingleFixedChoicePage {
+    public MultipleFixedChoicePage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return MultipleChoiceFragment.create(getKey());
+    }
+
+    @Override
+    public void getReviewItems(ArrayList<ReviewItem> dest) {
+        StringBuilder sb = new StringBuilder();
+
+        ArrayList<String> selections = mData.getStringArrayList(Page.SIMPLE_DATA_KEY);
+        if (selections != null && selections.size() > 0) {
+            for (String selection : selections) {
+                if (sb.length() > 0) {
+                    sb.append(", ");
+                }
+                sb.append(selection);
+            }
+        }
+
+        dest.add(new ReviewItem(getTitle(), sb.toString(), getKey()));
+    }
+
+    @Override
+    public boolean isCompleted() {
+        ArrayList<String> selections = mData.getStringArrayList(Page.SIMPLE_DATA_KEY);
+        return selections != null && selections.size() > 0;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/Page.java b/src/net/i2p/android/wizard/model/Page.java
new file mode 100644
index 0000000000000000000000000000000000000000..98cbdd6a2796b69b14b6c86ae310289298d2161a
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/Page.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents a single page in the wizard.
+ */
+public abstract class Page implements PageTreeNode {
+    /**
+     * The key into {@link #getData()} used for wizards with simple (single) values.
+     */
+    public static final String SIMPLE_DATA_KEY = "_";
+
+    protected ModelCallbacks mCallbacks;
+
+    /**
+     * Conditionals that rely on this page.
+     */
+    protected List<ModelCallbacks> mConditionals = new ArrayList<ModelCallbacks>();
+
+    /**
+     * Conditions on whether this page should be used.
+     */
+    protected List<Conditional.Condition> mConditions = new ArrayList<Conditional.Condition>();
+    /**
+     * Should all conditions be satisfied, or any of them?
+     */
+    protected boolean mConditionAnd = false;
+    /**
+     * The last condition status.
+     */
+    protected boolean mSatisfied = true;
+
+    /**
+     * Current wizard values/selections.
+     */
+    protected Bundle mData = new Bundle();
+    protected String mTitle;
+    protected boolean mRequired = false;
+    protected String mParentKey;
+
+    protected Page(ModelCallbacks callbacks, String title) {
+        mCallbacks = callbacks;
+        mTitle = title;
+    }
+
+    public Bundle getData() {
+        return mData;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public boolean isSatisfied() {
+        boolean ret = true;
+        if (mConditions.size() > 0) {
+            ret = false;
+            for (Conditional.Condition c : mConditions) {
+                if (c.isSatisfied()) {
+                    ret = true;
+                    if (!mConditionAnd) break;
+                } else if (mConditionAnd) {
+                    ret = false;
+                    break;
+                }
+            }
+        }
+        // If the conditions have changed, update the page tree.
+        if (!(mSatisfied == ret)) {
+            mSatisfied = ret;
+            mCallbacks.onPageTreeChanged();
+        }
+        return mSatisfied;
+    }
+
+    public boolean isRequired() {
+        return isSatisfied() && mRequired;
+    }
+
+    void setParentKey(String parentKey) {
+        mParentKey = parentKey;
+    }
+
+    public Page findByKey(String key) {
+        return getKey().equals(key) ? this : null;
+    }
+
+    public void flattenCurrentPageSequence(ArrayList<Page> dest) {
+        if (isSatisfied())
+            dest.add(this);
+    }
+
+    public abstract Fragment createFragment();
+
+    public String getKey() {
+        return (mParentKey != null) ? mParentKey + ":" + mTitle : mTitle;
+    }
+
+    public abstract void getReviewItems(ArrayList<ReviewItem> dest);
+
+    public boolean isCompleted() {
+        return true;
+    }
+
+    public void resetData(Bundle data) {
+        mData = data;
+        notifyDataChanged();
+    }
+
+    public void notifyDataChanged() {
+        for (ModelCallbacks c : mConditionals) {
+            c.onPageDataChanged(this);
+        }
+        mCallbacks.onPageDataChanged(this);
+    }
+
+    public Page setRequired(boolean required) {
+        mRequired = required;
+        return this;
+    }
+
+    public Page makeConditional(Conditional conditional) {
+        mConditionals.add(conditional);
+        return this;
+    }
+
+    public <T> Page setEqualCondition(Conditional conditional, T comp) {
+        Conditional.Condition c = conditional.new EqualCondition<T>(this, comp);
+        mConditions.add(c);
+        return this;
+    }
+
+    public <T> Page setNotEqualCondition(Conditional conditional, T comp) {
+        Conditional.Condition c = conditional.new NotEqualCondition<T>(this, comp);
+        mConditions.add(c);
+        return this;
+    }
+
+    public <T> Page setEqualAnyCondition(Conditional conditional, T... choices) {
+        Conditional.Condition c = conditional.new EqualAnyCondition<T>(this, choices);
+        mConditions.add(c);
+        return this;
+    }
+
+    public Page satisfyAllConditions(boolean conditionAnd) {
+        mConditionAnd = conditionAnd;
+        return this;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/PageList.java b/src/net/i2p/android/wizard/model/PageList.java
new file mode 100644
index 0000000000000000000000000000000000000000..27742b887d660bd22507d596221137bd18a7ae4b
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/PageList.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a list of wizard pages.
+ */
+public class PageList extends ArrayList<Page> implements PageTreeNode {
+    public PageList(Page... pages) {
+        for (Page page : pages) {
+            add(page);
+        }
+    }
+
+    public Page findByKey(String key) {
+        for (Page childPage : this) {
+            Page found = childPage.findByKey(key);
+            if (found != null) {
+                return found;
+            }
+        }
+
+        return null;
+    }
+
+    public void flattenCurrentPageSequence(ArrayList<Page> dest) {
+        for (Page childPage : this) {
+            childPage.flattenCurrentPageSequence(dest);
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/PageTreeNode.java b/src/net/i2p/android/wizard/model/PageTreeNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..49d595de13c0efc1384a467187066613aa2697f6
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/PageTreeNode.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a node in the page tree. Can either be a single page, or a page container.
+ */
+public interface PageTreeNode {
+    public Page findByKey(String key);
+    public void flattenCurrentPageSequence(ArrayList<Page> dest);
+}
diff --git a/src/net/i2p/android/wizard/model/ReviewItem.java b/src/net/i2p/android/wizard/model/ReviewItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3c00f6b316dec0d218895fb708618cbac19edb6
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/ReviewItem.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+/**
+ * Represents a single line item on the final review page.
+ *
+ * @see net.i2p.android.wizard.ui.ReviewFragment
+ */
+public class ReviewItem {
+    public static final int DEFAULT_WEIGHT = 0;
+
+    private int mWeight;
+    private String mTitle;
+    private String mDisplayValue;
+    private String mPageKey;
+
+    public ReviewItem(String title, String displayValue, String pageKey) {
+        this(title, displayValue, pageKey, DEFAULT_WEIGHT);
+    }
+
+    public ReviewItem(String title, String displayValue, String pageKey, int weight) {
+        mTitle = title;
+        mDisplayValue = displayValue;
+        mPageKey = pageKey;
+        mWeight = weight;
+    }
+
+    public String getDisplayValue() {
+        return mDisplayValue;
+    }
+
+    public void setDisplayValue(String displayValue) {
+        mDisplayValue = displayValue;
+    }
+
+    public String getPageKey() {
+        return mPageKey;
+    }
+
+    public void setPageKey(String pageKey) {
+        mPageKey = pageKey;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public void setTitle(String title) {
+        mTitle = title;
+    }
+
+    public int getWeight() {
+        return mWeight;
+    }
+
+    public void setWeight(int weight) {
+        mWeight = weight;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/SingleFixedBooleanPage.java b/src/net/i2p/android/wizard/model/SingleFixedBooleanPage.java
new file mode 100644
index 0000000000000000000000000000000000000000..d9625628a3ca4efd04c7a716d9a1def6fc99739f
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/SingleFixedBooleanPage.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import java.util.ArrayList;
+
+import net.i2p.android.wizard.ui.SingleBooleanFragment;
+
+import android.support.v4.app.Fragment;
+
+public class SingleFixedBooleanPage extends Page {
+    protected String mDesc = "";
+    protected String mLabel = null;
+
+    public SingleFixedBooleanPage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return SingleBooleanFragment.create(getKey());
+    }
+
+    @Override
+    public void getReviewItems(ArrayList<ReviewItem> dest) {
+        dest.add(new ReviewItem(getTitle(),
+                mData.getBoolean(SIMPLE_DATA_KEY) ? "Yes" : "No", getKey()));
+    }
+
+    public SingleFixedBooleanPage setLabel(String label) {
+        mLabel = label;
+        return this;
+    }
+
+    public String getLabel() {
+        return mLabel;
+    }
+
+    public SingleFixedBooleanPage setDescription(String desc) {
+        mDesc = desc;
+        return this;
+    }
+
+    public String getDesc() {
+        return mDesc;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/SingleFixedChoicePage.java b/src/net/i2p/android/wizard/model/SingleFixedChoicePage.java
new file mode 100644
index 0000000000000000000000000000000000000000..64bb4ea0505e2730388cf38206e2a1a79ea77f26
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/SingleFixedChoicePage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import net.i2p.android.wizard.ui.SingleChoiceFragment;
+
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * A page offering the user a number of mutually exclusive choices.
+ */
+public class SingleFixedChoicePage extends Page {
+    protected ArrayList<String> mChoices = new ArrayList<String>();
+
+    public SingleFixedChoicePage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return SingleChoiceFragment.create(getKey());
+    }
+
+    public String getOptionAt(int position) {
+        return mChoices.get(position);
+    }
+
+    public int getOptionCount() {
+        return mChoices.size();
+    }
+
+    @Override
+    public void getReviewItems(ArrayList<ReviewItem> dest) {
+        dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey()));
+    }
+
+    @Override
+    public boolean isCompleted() {
+        return !TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY));
+    }
+
+    public SingleFixedChoicePage setChoices(String... choices) {
+        mChoices.addAll(Arrays.asList(choices));
+        return this;
+    }
+
+    public SingleFixedChoicePage setValue(String value) {
+        mData.putString(SIMPLE_DATA_KEY, value);
+        return this;
+    }
+}
diff --git a/src/net/i2p/android/wizard/model/SingleTextFieldPage.java b/src/net/i2p/android/wizard/model/SingleTextFieldPage.java
new file mode 100644
index 0000000000000000000000000000000000000000..30e75958a5c63e94a32c699298c7eaf722c2697d
--- /dev/null
+++ b/src/net/i2p/android/wizard/model/SingleTextFieldPage.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.model;
+
+import net.i2p.android.wizard.ui.SingleTextFieldFragment;
+
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+/**
+ * A page asking for a text field.
+ */
+public class SingleTextFieldPage extends Page {
+    // The null is checked in SingleTextFieldFragment
+    protected String mDef = null;
+    protected String mDesc = "";
+
+    public SingleTextFieldPage(ModelCallbacks callbacks, String title) {
+        super(callbacks, title);
+    }
+
+    @Override
+    public Fragment createFragment() {
+        return SingleTextFieldFragment.create(getKey());
+    }
+
+    @Override
+    public void getReviewItems(ArrayList<ReviewItem> dest) {
+        dest.add(new ReviewItem(getTitle(), mData.getString(SIMPLE_DATA_KEY), getKey(), -1));
+    }
+
+    @Override
+    public boolean isCompleted() {
+        return (!TextUtils.isEmpty(mData.getString(SIMPLE_DATA_KEY))) && isValid();
+    }
+
+    public SingleTextFieldPage setDefault(String def) {
+        mDef = def;
+        return this;
+    }
+
+    public String getDefault() {
+        return mDef;
+    }
+
+    public SingleTextFieldPage setDescription(String desc) {
+        mDesc = desc;
+        return this;
+    }
+
+    public String getDesc() {
+        return mDesc;
+    }
+
+    // Override these in subclasses to add content verification.
+
+    public boolean isValid() {
+        return true;
+    }
+
+    public boolean showFeedback() {
+        return false;
+    }
+
+    public String getFeedback() {
+        return "";
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/I2PDestinationFragment.java b/src/net/i2p/android/wizard/ui/I2PDestinationFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..271e15e9f4c0527c27a1ecd82c0087dd75ace154
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/I2PDestinationFragment.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.router.activity.AddressbookActivity;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.model.SingleTextFieldPage;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.TextView;
+
+public class I2PDestinationFragment extends Fragment {
+    static final int REQUEST_ADDRESSBOOK_DOMAIN = 1;
+
+    private static final String ARG_KEY = "key";
+
+    private PageFragmentCallbacks mCallbacks;
+    private String mKey;
+    private SingleTextFieldPage mPage;
+    protected TextView mFieldView;
+    private TextView mFeedbackView;
+
+    public static I2PDestinationFragment create(String key) {
+        Bundle args = new Bundle();
+        args.putString(ARG_KEY, key);
+
+        I2PDestinationFragment fragment = new I2PDestinationFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public I2PDestinationFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Bundle args = getArguments();
+        mKey = args.getString(ARG_KEY);
+        mPage = (SingleTextFieldPage) mCallbacks.onGetPage(mKey);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page_single_text_field_picker, container, false);
+        ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle());
+        ((TextView) rootView.findViewById(R.id.wizard_text_field_desc)).setText(mPage.getDesc());
+
+        Button b = (Button) rootView.findViewById(R.id.wizard_text_field_pick);
+        b.setOnClickListener(new View.OnClickListener() {
+
+            public void onClick(View view) {
+                Intent ai = new Intent(getActivity(), AddressbookActivity.class);
+                ai.setAction(Intent.ACTION_PICK);
+                ai.setData(Uri.parse("content://net.i2p.addressbook/domains"));
+                startActivityForResult(ai, REQUEST_ADDRESSBOOK_DOMAIN);
+            }
+        });
+
+        mFieldView = ((TextView) rootView.findViewById(R.id.wizard_text_field));
+        mFieldView.setHint(mPage.getTitle());
+        if (mPage.getData().getString(Page.SIMPLE_DATA_KEY) != null)
+            mFieldView.setText(mPage.getData().getString(Page.SIMPLE_DATA_KEY));
+        else if (mPage.getDefault() != null) {
+            mFieldView.setText(mPage.getDefault());
+            mPage.getData().putString(Page.SIMPLE_DATA_KEY, mPage.getDefault());
+        }
+
+        mFeedbackView = (TextView) rootView.findViewById(R.id.wizard_text_field_feedback);
+
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof PageFragmentCallbacks)) {
+            throw new ClassCastException("Activity must implement PageFragmentCallbacks");
+        }
+
+        mCallbacks = (PageFragmentCallbacks) activity;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mFieldView.addTextChangedListener(new TextWatcher() {
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1,
+                    int i2) {
+            }
+
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+            }
+
+            public void afterTextChanged(Editable editable) {
+                mPage.getData().putString(Page.SIMPLE_DATA_KEY,
+                        (editable != null) ? editable.toString() : null);
+                mPage.notifyDataChanged();
+                if (mPage.showFeedback()) {
+                    mFeedbackView.setText(mPage.getFeedback());
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setMenuVisibility(boolean menuVisible) {
+        super.setMenuVisibility(menuVisible);
+
+        // In a future update to the support library, this should override setUserVisibleHint
+        // instead of setMenuVisibility.
+        if (mFieldView != null) {
+            InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
+                    Context.INPUT_METHOD_SERVICE);
+            if (!menuVisible) {
+                imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
+            }
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_ADDRESSBOOK_DOMAIN) {
+            if (resultCode == Activity.RESULT_OK) {
+                Uri result = data.getData();
+                mFieldView.setText(result.getHost());
+            }
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java b/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..05e6e458c7d687712cab772ff9187815760d2155
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/MultipleChoiceFragment.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.MultipleFixedChoicePage;
+import net.i2p.android.wizard.model.Page;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ListFragment;
+import android.util.SparseBooleanArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MultipleChoiceFragment extends ListFragment {
+    private static final String ARG_KEY = "key";
+
+    private PageFragmentCallbacks mCallbacks;
+    private String mKey;
+    private List<String> mChoices;
+    private Page mPage;
+
+    public static MultipleChoiceFragment create(String key) {
+        Bundle args = new Bundle();
+        args.putString(ARG_KEY, key);
+
+        MultipleChoiceFragment fragment = new MultipleChoiceFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public MultipleChoiceFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Bundle args = getArguments();
+        mKey = args.getString(ARG_KEY);
+        mPage = mCallbacks.onGetPage(mKey);
+
+        MultipleFixedChoicePage fixedChoicePage = (MultipleFixedChoicePage) mPage;
+        mChoices = new ArrayList<String>();
+        for (int i = 0; i < fixedChoicePage.getOptionCount(); i++) {
+            mChoices.add(fixedChoicePage.getOptionAt(i));
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page, container, false);
+        ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle());
+
+        final ListView listView = (ListView) rootView.findViewById(android.R.id.list);
+        setListAdapter(new ArrayAdapter<String>(getActivity(),
+                android.R.layout.simple_list_item_multiple_choice,
+                android.R.id.text1,
+                mChoices));
+        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+        // Pre-select currently selected items.
+        new Handler().post(new Runnable() {
+            public void run() {
+                ArrayList<String> selectedItems = mPage.getData().getStringArrayList(
+                        Page.SIMPLE_DATA_KEY);
+                if (selectedItems == null || selectedItems.size() == 0) {
+                    return;
+                }
+
+                Set<String> selectedSet = new HashSet<String>(selectedItems);
+
+                for (int i = 0; i < mChoices.size(); i++) {
+                    if (selectedSet.contains(mChoices.get(i))) {
+                        listView.setItemChecked(i, true);
+                    }
+                }
+            }
+        });
+
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof PageFragmentCallbacks)) {
+            throw new ClassCastException("Activity must implement PageFragmentCallbacks");
+        }
+
+        mCallbacks = (PageFragmentCallbacks) activity;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        SparseBooleanArray checkedPositions = getListView().getCheckedItemPositions();
+        ArrayList<String> selections = new ArrayList<String>();
+        for (int i = 0; i < checkedPositions.size(); i++) {
+            if (checkedPositions.valueAt(i)) {
+                selections.add(getListAdapter().getItem(checkedPositions.keyAt(i)).toString());
+            }
+        }
+
+        mPage.getData().putStringArrayList(Page.SIMPLE_DATA_KEY, selections);
+        mPage.notifyDataChanged();
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java b/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0cfae019e7c628627282dd7129d969d59f5db2d
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/PageFragmentCallbacks.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.wizard.model.Page;
+
+public interface PageFragmentCallbacks {
+    Page onGetPage(String key);
+}
diff --git a/src/net/i2p/android/wizard/ui/ReviewFragment.java b/src/net/i2p/android/wizard/ui/ReviewFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..d956476d79c49447c1084c9702b09ac9933eb17a
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/ReviewFragment.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.AbstractWizardModel;
+import net.i2p.android.wizard.model.ModelCallbacks;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.model.ReviewItem;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.ListFragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class ReviewFragment extends ListFragment implements ModelCallbacks {
+    private Callbacks mCallbacks;
+    private AbstractWizardModel mWizardModel;
+    private List<ReviewItem> mCurrentReviewItems;
+
+    private ReviewAdapter mReviewAdapter;
+
+    public ReviewFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mReviewAdapter = new ReviewAdapter();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page, container, false);
+
+        TextView titleView = (TextView) rootView.findViewById(android.R.id.title);
+        titleView.setText(R.string.review);
+        titleView.setTextColor(getResources().getColor(R.color.review_green));
+
+        ListView listView = (ListView) rootView.findViewById(android.R.id.list);
+        setListAdapter(mReviewAdapter);
+        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof Callbacks)) {
+            throw new ClassCastException("Activity must implement fragment's callbacks");
+        }
+
+        mCallbacks = (Callbacks) activity;
+
+        mWizardModel = mCallbacks.onGetModel();
+        mWizardModel.registerListener(this);
+        onPageTreeChanged();
+    }
+
+    public void onPageTreeChanged() {
+        onPageDataChanged(null);
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+
+        mWizardModel.unregisterListener(this);
+    }
+
+    public void onPageDataChanged(Page changedPage) {
+        ArrayList<ReviewItem> reviewItems = new ArrayList<ReviewItem>();
+        for (Page page : mWizardModel.getCurrentPageSequence()) {
+            page.getReviewItems(reviewItems);
+        }
+        Collections.sort(reviewItems, new Comparator<ReviewItem>() {
+            public int compare(ReviewItem a, ReviewItem b) {
+                return a.getWeight() > b.getWeight() ? +1 : a.getWeight() < b.getWeight() ? -1 : 0;
+            }
+        });
+        mCurrentReviewItems = reviewItems;
+
+        if (mReviewAdapter != null) {
+            mReviewAdapter.notifyDataSetInvalidated();
+        }
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        mCallbacks.onEditScreenAfterReview(mCurrentReviewItems.get(position).getPageKey());
+    }
+
+    public interface Callbacks {
+        AbstractWizardModel onGetModel();
+        void onEditScreenAfterReview(String pageKey);
+    }
+
+    private class ReviewAdapter extends BaseAdapter {
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return true;
+        }
+
+        public Object getItem(int position) {
+            return mCurrentReviewItems.get(position);
+        }
+
+        public long getItemId(int position) {
+            return mCurrentReviewItems.get(position).hashCode();
+        }
+
+        public View getView(int position, View view, ViewGroup container) {
+            LayoutInflater inflater = LayoutInflater.from(getActivity());
+            View rootView = inflater.inflate(R.layout.listitem_wizard_review, container, false);
+
+            ReviewItem reviewItem = mCurrentReviewItems.get(position);
+            String value = reviewItem.getDisplayValue();
+            if (TextUtils.isEmpty(value)) {
+                value = "(None)";
+            }
+            ((TextView) rootView.findViewById(android.R.id.text1)).setText(reviewItem.getTitle());
+            ((TextView) rootView.findViewById(android.R.id.text2)).setText(value);
+            return rootView;
+        }
+
+        public int getCount() {
+            return mCurrentReviewItems.size();
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/SingleBooleanFragment.java b/src/net/i2p/android/wizard/ui/SingleBooleanFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..d4121daaa311ca55f2169aa661117cb78c9dc8a5
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/SingleBooleanFragment.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.model.SingleFixedBooleanPage;
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+public class SingleBooleanFragment extends Fragment {
+    private static final String ARG_KEY = "key";
+
+    private PageFragmentCallbacks mCallbacks;
+    private String mKey;
+    private SingleFixedBooleanPage mPage;
+    private CheckBox mCheckBox;
+
+    public static SingleBooleanFragment create(String key) {
+        Bundle args = new Bundle();
+        args.putString(ARG_KEY, key);
+
+        SingleBooleanFragment fragment = new SingleBooleanFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public SingleBooleanFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Bundle args = getArguments();
+        mKey = args.getString(ARG_KEY);
+        mPage = (SingleFixedBooleanPage) mCallbacks.onGetPage(mKey);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page_single_boolean, container, false);
+        ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle());
+        ((TextView) rootView.findViewById(R.id.wizard_text_field_desc)).setText(mPage.getDesc());
+
+        mCheckBox = ((CheckBox) rootView.findViewById(R.id.wizard_check_box));
+        mCheckBox.setChecked(mPage.getData().getBoolean(Page.SIMPLE_DATA_KEY));
+        if (mPage.getLabel() != null)
+            mCheckBox.setText(mPage.getLabel());
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof PageFragmentCallbacks)) {
+            throw new ClassCastException("Activity must implement PageFragmentCallbacks");
+        }
+
+        mCallbacks = (PageFragmentCallbacks) activity;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                mPage.getData().putBoolean(Page.SIMPLE_DATA_KEY, isChecked);
+                mPage.notifyDataChanged();
+            }
+        });
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java b/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..93a419c6748a5029ff035307e4918d4cc6926cd2
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/SingleChoiceFragment.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2013 Google Inc.
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.model.SingleFixedChoicePage;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v4.app.ListFragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SingleChoiceFragment extends ListFragment {
+    private static final String ARG_KEY = "key";
+
+    private PageFragmentCallbacks mCallbacks;
+    private List<String> mChoices;
+    private String mKey;
+    private Page mPage;
+
+    public static SingleChoiceFragment create(String key) {
+        Bundle args = new Bundle();
+        args.putString(ARG_KEY, key);
+
+        SingleChoiceFragment fragment = new SingleChoiceFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public SingleChoiceFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Bundle args = getArguments();
+        mKey = args.getString(ARG_KEY);
+        mPage = mCallbacks.onGetPage(mKey);
+
+        SingleFixedChoicePage fixedChoicePage = (SingleFixedChoicePage) mPage;
+        mChoices = new ArrayList<String>();
+        for (int i = 0; i < fixedChoicePage.getOptionCount(); i++) {
+            mChoices.add(fixedChoicePage.getOptionAt(i));
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page, container, false);
+        ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle());
+
+        final ListView listView = (ListView) rootView.findViewById(android.R.id.list);
+        setListAdapter(new ArrayAdapter<String>(getActivity(),
+                android.R.layout.simple_list_item_single_choice,
+                android.R.id.text1,
+                mChoices));
+        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+
+        // Pre-select currently selected item.
+        new Handler().post(new Runnable() {
+            public void run() {
+                String selection = mPage.getData().getString(Page.SIMPLE_DATA_KEY);
+                for (int i = 0; i < mChoices.size(); i++) {
+                    if (mChoices.get(i).equals(selection)) {
+                        listView.setItemChecked(i, true);
+                        break;
+                    }
+                }
+            }
+        });
+
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof PageFragmentCallbacks)) {
+            throw new ClassCastException("Activity must implement PageFragmentCallbacks");
+        }
+
+        mCallbacks = (PageFragmentCallbacks) activity;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        mPage.getData().putString(Page.SIMPLE_DATA_KEY,
+                getListAdapter().getItem(position).toString());
+        mPage.notifyDataChanged();
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/SingleTextFieldFragment.java b/src/net/i2p/android/wizard/ui/SingleTextFieldFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..6566f17851f1cc679c494da90a96e0ffb25e0074
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/SingleTextFieldFragment.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2013 str4d
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.android.wizard.model.SingleTextFieldPage;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+
+public class SingleTextFieldFragment extends Fragment {
+    private static final String ARG_KEY = "key";
+
+    private PageFragmentCallbacks mCallbacks;
+    private String mKey;
+    private SingleTextFieldPage mPage;
+    private TextView mFieldView;
+    private TextView mFeedbackView;
+
+    public static SingleTextFieldFragment create(String key) {
+        Bundle args = new Bundle();
+        args.putString(ARG_KEY, key);
+
+        SingleTextFieldFragment fragment = new SingleTextFieldFragment();
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    public SingleTextFieldFragment() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Bundle args = getArguments();
+        mKey = args.getString(ARG_KEY);
+        mPage = (SingleTextFieldPage) mCallbacks.onGetPage(mKey);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View rootView = inflater.inflate(R.layout.fragment_wizard_page_single_text_field, container, false);
+        ((TextView) rootView.findViewById(android.R.id.title)).setText(mPage.getTitle());
+        ((TextView) rootView.findViewById(R.id.wizard_text_field_desc)).setText(mPage.getDesc());
+
+        mFieldView = ((TextView) rootView.findViewById(R.id.wizard_text_field));
+        mFieldView.setHint(mPage.getTitle());
+        if (mPage.getData().getString(Page.SIMPLE_DATA_KEY) != null)
+            mFieldView.setText(mPage.getData().getString(Page.SIMPLE_DATA_KEY));
+        else if (mPage.getDefault() != null) {
+            mFieldView.setText(mPage.getDefault());
+            mPage.getData().putString(Page.SIMPLE_DATA_KEY, mPage.getDefault());
+        }
+
+        mFeedbackView = (TextView) rootView.findViewById(R.id.wizard_text_field_feedback);
+
+        return rootView;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        if (!(activity instanceof PageFragmentCallbacks)) {
+            throw new ClassCastException("Activity must implement PageFragmentCallbacks");
+        }
+
+        mCallbacks = (PageFragmentCallbacks) activity;
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        mCallbacks = null;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mFieldView.addTextChangedListener(new TextWatcher() {
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1,
+                    int i2) {
+            }
+
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+            }
+
+            public void afterTextChanged(Editable editable) {
+                mPage.getData().putString(Page.SIMPLE_DATA_KEY,
+                        (editable != null) ? editable.toString() : null);
+                mPage.notifyDataChanged();
+                if (mPage.showFeedback()) {
+                    mFeedbackView.setText(mPage.getFeedback());
+                }
+            }
+        });
+    }
+
+    @Override
+    public void setMenuVisibility(boolean menuVisible) {
+        super.setMenuVisibility(menuVisible);
+
+        // In a future update to the support library, this should override setUserVisibleHint
+        // instead of setMenuVisibility.
+        if (mFieldView != null) {
+            InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
+                    Context.INPUT_METHOD_SERVICE);
+            if (!menuVisible) {
+                imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
+            }
+        }
+    }
+}
diff --git a/src/net/i2p/android/wizard/ui/StepPagerStrip.java b/src/net/i2p/android/wizard/ui/StepPagerStrip.java
new file mode 100644
index 0000000000000000000000000000000000000000..40c35dce7cadba4ca44e5b14059e8c3d8a79f0fe
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/StepPagerStrip.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.i2p.android.wizard.ui;
+
+import net.i2p.android.router.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class StepPagerStrip extends View {
+    private static final int[] ATTRS = new int[]{
+            android.R.attr.gravity
+    };
+    private int mPageCount;
+    private int mCurrentPage;
+
+    private int mGravity = Gravity.LEFT | Gravity.TOP;
+    private float mTabWidth;
+    private float mTabHeight;
+    private float mTabSpacing;
+
+    private Paint mPrevTabPaint;
+    private Paint mSelectedTabPaint;
+    private Paint mSelectedLastTabPaint;
+    private Paint mNextTabPaint;
+
+    private RectF mTempRectF = new RectF();
+
+    //private Scroller mScroller;
+
+    private OnPageSelectedListener mOnPageSelectedListener;
+
+    public StepPagerStrip(Context context) {
+        this(context, null, 0);
+    }
+
+    public StepPagerStrip(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public StepPagerStrip(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+        mGravity = a.getInteger(0, mGravity);
+        a.recycle();
+
+        final Resources res = getResources();
+        mTabWidth = res.getDimensionPixelSize(R.dimen.step_pager_tab_width);
+        mTabHeight = res.getDimensionPixelSize(R.dimen.step_pager_tab_height);
+        mTabSpacing = res.getDimensionPixelSize(R.dimen.step_pager_tab_spacing);
+
+        mPrevTabPaint = new Paint();
+        mPrevTabPaint.setColor(res.getColor(R.color.step_pager_previous_tab_color));
+
+        mSelectedTabPaint = new Paint();
+        mSelectedTabPaint.setColor(res.getColor(R.color.step_pager_selected_tab_color));
+
+        mSelectedLastTabPaint = new Paint();
+        mSelectedLastTabPaint.setColor(res.getColor(R.color.step_pager_selected_last_tab_color));
+
+        mNextTabPaint = new Paint();
+        mNextTabPaint.setColor(res.getColor(R.color.step_pager_next_tab_color));
+    }
+
+    public void setOnPageSelectedListener(OnPageSelectedListener onPageSelectedListener) {
+        mOnPageSelectedListener = onPageSelectedListener;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mPageCount == 0) {
+            return;
+        }
+
+        float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing;
+        float totalLeft;
+        boolean fillHorizontal = false;
+
+        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.CENTER_HORIZONTAL:
+                totalLeft = (getWidth() - totalWidth) / 2;
+                break;
+            case Gravity.RIGHT:
+                totalLeft = getWidth() - getPaddingRight() - totalWidth;
+                break;
+            case Gravity.FILL_HORIZONTAL:
+                totalLeft = getPaddingLeft();
+                fillHorizontal = true;
+                break;
+            default:
+                totalLeft = getPaddingLeft();
+        }
+
+        switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+            case Gravity.CENTER_VERTICAL:
+                mTempRectF.top = (int) (getHeight() - mTabHeight) / 2;
+                break;
+            case Gravity.BOTTOM:
+                mTempRectF.top = getHeight() - getPaddingBottom() - mTabHeight;
+                break;
+            default:
+                mTempRectF.top = getPaddingTop();
+        }
+
+        mTempRectF.bottom = mTempRectF.top + mTabHeight;
+
+        float tabWidth = mTabWidth;
+        if (fillHorizontal) {
+            tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft()
+                    - (mPageCount - 1) * mTabSpacing) / mPageCount;
+        }
+
+        for (int i = 0; i < mPageCount; i++) {
+            mTempRectF.left = totalLeft + (i * (tabWidth + mTabSpacing));
+            mTempRectF.right = mTempRectF.left + tabWidth;
+            canvas.drawRect(mTempRectF, i < mCurrentPage
+                    ? mPrevTabPaint
+                    : (i > mCurrentPage
+                            ? mNextTabPaint
+                            : (i == mPageCount - 1
+                                    ? mSelectedLastTabPaint
+                                    : mSelectedTabPaint)));
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(
+                View.resolveSize(
+                        (int) (mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing)
+                                + getPaddingLeft() + getPaddingRight(),
+                        widthMeasureSpec),
+                View.resolveSize(
+                        (int) mTabHeight
+                                + getPaddingTop() + getPaddingBottom(),
+                        heightMeasureSpec));
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        scrollCurrentPageIntoView();
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mOnPageSelectedListener != null) {
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_MOVE:
+                    int position = hitTest(event.getX());
+                    if (position >= 0) {
+                        mOnPageSelectedListener.onPageStripSelected(position);
+                    }
+                    return true;
+            }
+        }
+        return super.onTouchEvent(event);
+    }
+
+    private int hitTest(float x) {
+        if (mPageCount == 0) {
+            return -1;
+        }
+
+        float totalWidth = mPageCount * (mTabWidth + mTabSpacing) - mTabSpacing;
+        float totalLeft;
+        boolean fillHorizontal = false;
+
+        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.CENTER_HORIZONTAL:
+                totalLeft = (getWidth() - totalWidth) / 2;
+                break;
+            case Gravity.RIGHT:
+                totalLeft = getWidth() - getPaddingRight() - totalWidth;
+                break;
+            case Gravity.FILL_HORIZONTAL:
+                totalLeft = getPaddingLeft();
+                fillHorizontal = true;
+                break;
+            default:
+                totalLeft = getPaddingLeft();
+        }
+
+        float tabWidth = mTabWidth;
+        if (fillHorizontal) {
+            tabWidth = (getWidth() - getPaddingRight() - getPaddingLeft()
+                    - (mPageCount - 1) * mTabSpacing) / mPageCount;
+        }
+
+        float totalRight = totalLeft + (mPageCount * (tabWidth + mTabSpacing));
+        if (x >= totalLeft && x <= totalRight && totalRight > totalLeft) {
+            return (int) (((x - totalLeft) / (totalRight - totalLeft)) * mPageCount);
+        } else {
+            return -1;
+        }
+    }
+
+    public void setCurrentPage(int currentPage) {
+        mCurrentPage = currentPage;
+        invalidate();
+        scrollCurrentPageIntoView();
+
+        // TODO: Set content description appropriately
+    }
+
+    private void scrollCurrentPageIntoView() {
+        // TODO: only works with left gravity for now
+//
+//        float widthToActive = getPaddingLeft() + (mCurrentPage + 1) * (mTabWidth + mTabSpacing)
+//                - mTabSpacing;
+//        int viewWidth = getWidth();
+//
+//        int startScrollX = getScrollX();
+//        int destScrollX = (widthToActive > viewWidth) ? (int) (widthToActive - viewWidth) : 0;
+//
+//        if (mScroller == null) {
+//            mScroller = new Scroller(getContext());
+//        }
+//
+//        mScroller.abortAnimation();
+//        mScroller.startScroll(startScrollX, 0, destScrollX - startScrollX, 0);
+//        postInvalidate();
+    }
+
+    public void setPageCount(int count) {
+        mPageCount = count;
+        invalidate();
+
+        // TODO: Set content description appropriately
+    }
+
+    public static interface OnPageSelectedListener {
+        void onPageStripSelected(int position);
+    }
+
+//
+//    @Override
+//    public void computeScroll() {
+//        super.computeScroll();
+//        if (mScroller.computeScrollOffset()) {
+//            setScrollX(mScroller.getCurrX());
+//        }
+//    }
+}