diff --git a/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java
index 8a977ebcb4c5fa1fd87bf478e1a73941f06cfb0f..bac7e1978607ba87fc166cd4466388ff89bb3f84 100644
--- a/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java
+++ b/src/net/i2p/android/i2ptunnel/activity/TunnelWizardActivity.java
@@ -1,15 +1,9 @@
 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 net.i2p.android.wizard.ui.AbstractWizardActivity;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -17,256 +11,34 @@ 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);
-    }
 
+public class TunnelWizardActivity extends AbstractWizardActivity {
     @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        mWizardModel.unregisterListener(this);
+    protected AbstractWizardModel onCreateModel() {
+        return new TunnelWizardModel(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;
-        }
+    protected DialogFragment onGetFinishWizardDialog() {
+        return 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();
+            }
+        };
     }
 }
diff --git a/src/net/i2p/android/wizard/ui/AbstractWizardActivity.java b/src/net/i2p/android/wizard/ui/AbstractWizardActivity.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b390d8138461561e1eb267e80f74d553622982e
--- /dev/null
+++ b/src/net/i2p/android/wizard/ui/AbstractWizardActivity.java
@@ -0,0 +1,251 @@
+package net.i2p.android.wizard.ui;
+
+import java.util.List;
+
+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.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 abstract class AbstractWizardActivity extends FragmentActivity implements
+        PageFragmentCallbacks,
+        ReviewFragment.Callbacks,
+        ModelCallbacks {
+    private ViewPager mPager;
+    private MyPagerAdapter mPagerAdapter;
+
+    private boolean mEditingAfterReview;
+
+    protected AbstractWizardModel mWizardModel;
+
+    private boolean mConsumePageSelectedEvent;
+
+    private Button mNextButton;
+    private Button mPrevButton;
+
+    private List<Page> mCurrentPageSequence;
+    private StepPagerStrip mStepPagerStrip;
+
+    protected abstract AbstractWizardModel onCreateModel();
+
+    protected abstract DialogFragment onGetFinishWizardDialog();
+
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_wizard);
+
+        mWizardModel = onCreateModel();
+        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 = onGetFinishWizardDialog();
+                    dg.show(getSupportFragmentManager(), "finish_wizard_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;
+        }
+    }
+}