diff --git a/src/net/i2p/android/router/adapter/LogAdapter.java b/src/net/i2p/android/router/adapter/LogAdapter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a00fdfac4b40ee9fcd72e0f9451c4ff2323167b3
--- /dev/null
+++ b/src/net/i2p/android/router/adapter/LogAdapter.java
@@ -0,0 +1,24 @@
+package net.i2p.android.router.adapter;
+
+import java.util.List;
+
+import net.i2p.android.router.R;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+
+public class LogAdapter extends ArrayAdapter<String> {
+
+    public LogAdapter(Context context) {
+        super(context, R.layout.logs_list_item);
+    }
+
+    public void setData(List<String> entries) {
+        clear();
+        if (entries != null) {
+            for (String entry : entries) {
+                add(entry);
+            }
+        }
+    }
+}
diff --git a/src/net/i2p/android/router/fragment/LogFragment.java b/src/net/i2p/android/router/fragment/LogFragment.java
index ac2298b3d387ad7622259949cd12ae58862f0185..a2e6be006dcaf5edbde95c66775c57beb59c5bd9 100644
--- a/src/net/i2p/android/router/fragment/LogFragment.java
+++ b/src/net/i2p/android/router/fragment/LogFragment.java
@@ -1,26 +1,38 @@
 package net.i2p.android.router.fragment;
 
 import android.os.Bundle;
-import android.os.Handler;
 import android.support.v4.app.ListFragment;
-import android.widget.ArrayAdapter;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.View;
 import android.widget.ListView;
 import android.widget.TextView;
-import java.util.Collections;
 import java.util.List;
 import net.i2p.I2PAppContext;
 import net.i2p.android.router.R;
+import net.i2p.android.router.adapter.LogAdapter;
+import net.i2p.android.router.loader.LogLoader;
 
-public class LogFragment extends ListFragment {
+public class LogFragment extends ListFragment implements
+        LoaderManager.LoaderCallbacks<List<String>> {
+    public static final String LOG_LEVEL = "log_level";
+    /**
+     * 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";
 
-    String _logLevel;
-    private Handler _handler;
-    private Runnable _updater;
-    private ArrayAdapter<String> _adap;
-    private TextView _headerView;
+    private static final int LEVEL_ERROR = 1;
+    private static final int LEVEL_ALL = 2;
 
-    public static final String LOG_LEVEL = "log_level";
-    private static final int MAX = 250;
+    private LogAdapter mAdapter;
+    private TextView mHeaderView;
+    private String mLogLevel;
+    /**
+     * The current activated item position. Only used on tablets.
+     */
+    private int mActivatedPosition = ListView.INVALID_POSITION;
+    private boolean mActivateOnItemClick = false;
 
     public static LogFragment newInstance(String level) {
         LogFragment f = new LogFragment();
@@ -30,107 +42,75 @@ public class LogFragment extends ListFragment {
         return f;
     }
 
+    @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 LogAdapter(getActivity());
+        mLogLevel = getArguments().getString(LOG_LEVEL);
+
+        // set the header
+        mHeaderView = (TextView) getActivity().getLayoutInflater().inflate(R.layout.logs_header, null);
+        getListView().addHeaderView(mHeaderView, "", false);
+
+        setListAdapter(mAdapter);
 
-        // Grab context if router has started, otherwise create new
-        // FIXME dup contexts, locking, ...
-        List<String> msgs;
-        String header;
         I2PAppContext ctx = I2PAppContext.getCurrentContext();
         if (ctx != null) {
-            Bundle args = getArguments();
-            _logLevel = args.getString(LOG_LEVEL);
-            if ("ERROR".equals(_logLevel)) {
-                msgs = ctx.logManager().getBuffer().getMostRecentCriticalMessages();
-            } else {
-                msgs = ctx.logManager().getBuffer().getMostRecentMessages();
-            }
-            int sz = msgs.size();
-            header = getHeader(sz, ("ERROR".equals(_logLevel)));
-            if (sz > 1) {
-                Collections.reverse(msgs);
-            }
-        } else {
-            //msgs = Collections.EMPTY_LIST;
-            msgs = Collections.emptyList();
-            header = "No messages, router has not started yet.";
-        }
-
-        // set the header
-        _headerView = (TextView) getActivity().getLayoutInflater().inflate(R.layout.logs_header, null);
-        _headerView.setText(header);
-        ListView lv = getListView();
-        lv.addHeaderView(_headerView, "", false);
-        _adap = new ArrayAdapter<String>(getActivity(), R.layout.logs_list_item, msgs);
-        setListAdapter(_adap);
-
-/***
-        // set the callback
-        lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            public void onItemClick(AdapterView parent, View view, int pos, long id) {
-                // make it bigger or something
-            }
-        });
-***/
+            setEmptyText("ERROR".equals(mLogLevel) ?
+                    "No error messages" : "No messages");
 
-        _handler = new Handler();
-        _updater = new Updater();
+            setListShown(false);
+            getLoaderManager().initLoader("ERROR".equals(mLogLevel) ?
+                    LEVEL_ERROR : LEVEL_ALL, null, this);
+        } else
+            setEmptyText(getResources().getString(
+                    R.string.router_not_running));
     }
 
     @Override
-    public void onStart() {
-        super.onStart();
-        _handler.removeCallbacks(_updater);
-        _handler.postDelayed(_updater, 10*1000);
+    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 onStop() {
-        super.onStop();
-        _handler.removeCallbacks(_updater);
+    /**
+     * 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 class Updater implements Runnable {
-        public void run() {
-            I2PAppContext ctx = I2PAppContext.getCurrentContext();
-            if (ctx != null) {
-                List<String> msgs;
-                if ("ERROR".equals(_logLevel)) {
-                    msgs = ctx.logManager().getBuffer().getMostRecentCriticalMessages();
-                } else {
-                    msgs = ctx.logManager().getBuffer().getMostRecentMessages();
-                }
-		int sz = msgs.size();
-                if (sz > 0) {
-                    Collections.reverse(msgs);
-                    String oldNewest = _adap.getCount() > 0 ? _adap.getItem(0) : null;
-                    boolean changed = false;
-                    for (int i = 0; i < sz; i++) {
-                        String newItem = msgs.get(i);
-                        if (newItem.equals(oldNewest))
-                            break;
-                        _adap.insert(newItem, i);
-                        changed = true;
-                    }
-                    int newSz = _adap.getCount();
-                    for (int i = newSz - 1; i > MAX; i--) {
-                        _adap.remove(_adap.getItem(i));
-                    }
-                    if (changed) {
-                        // fixme update header
-                        newSz = _adap.getCount();
-                        String header = getHeader(newSz, (_logLevel == "ERROR"));
-                        _headerView.setText(header);
-                        _adap.notifyDataSetChanged();
-                    }
-                }
-            }
-            // LogWriter only processes queue every 10 seconds
-            _handler.postDelayed(this, 10*1000);
+    private void setActivatedPosition(int position) {
+        if (position == ListView.INVALID_POSITION) {
+            getListView().setItemChecked(mActivatedPosition, false);
+        } else {
+            getListView().setItemChecked(position, true);
         }
+
+        mActivatedPosition = position;
     }
 
     /** fixme plurals */
@@ -148,4 +128,34 @@ public class LogFragment extends ListFragment {
             return "1 message";
         return sz + " messages, newest first";
     }
+
+    // LoaderManager.LoaderCallbacks<List<String>>
+
+    public Loader<List<String>> onCreateLoader(int id, Bundle args) {
+        return new LogLoader(getActivity(),
+                I2PAppContext.getCurrentContext(), mLogLevel);
+    }
+
+    public void onLoadFinished(Loader<List<String>> loader,
+            List<String> data) {
+        if (loader.getId() == ("ERROR".equals(mLogLevel) ?
+                LEVEL_ERROR : LEVEL_ALL)) {
+            mAdapter.setData(data);
+            String header = getHeader(data.size(), (mLogLevel == "ERROR"));
+            mHeaderView.setText(header);
+
+            if (isResumed()) {
+                setListShown(true);
+            } else {
+                setListShownNoAnimation(true);
+            }
+        }
+    }
+
+    public void onLoaderReset(Loader<List<String>> loader) {
+        if (loader.getId() == ("ERROR".equals(mLogLevel) ?
+                LEVEL_ERROR : LEVEL_ALL)) {
+            mAdapter.setData(null);
+        }
+    }
 }
diff --git a/src/net/i2p/android/router/loader/LogLoader.java b/src/net/i2p/android/router/loader/LogLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..aaddbb69e9d08e6537b80fa3a2a3fd937cb89de6
--- /dev/null
+++ b/src/net/i2p/android/router/loader/LogLoader.java
@@ -0,0 +1,132 @@
+package net.i2p.android.router.loader;
+
+import java.util.Collections;
+import java.util.List;
+
+import net.i2p.I2PAppContext;
+
+import android.content.Context;
+import android.support.v4.content.AsyncTaskLoader;
+
+public class LogLoader extends AsyncTaskLoader<List<String>> {
+    private I2PAppContext mCtx;
+    private String mLogLevel;
+    private List<String> mData;
+
+    private static final int MAX_LOG_LENGTH = 250;
+
+    public LogLoader(Context context, I2PAppContext ctx, String logLevel) {
+        super(context);
+        mCtx = ctx;
+        mLogLevel = logLevel;
+    }
+
+    @Override
+    public List<String> loadInBackground() {
+        List<String> msgs;
+        if ("ERROR".equals(mLogLevel)) {
+            msgs = mCtx.logManager().getBuffer().getMostRecentCriticalMessages();
+        } else {
+            msgs = mCtx.logManager().getBuffer().getMostRecentMessages();
+        }
+        int sz = msgs.size();
+        if (sz > 1)
+            Collections.reverse(msgs);
+        if (sz > 0 && mData != null) {
+            String oldNewest = mData.size() > 0 ? mData.get(0) : null;
+            for (int i = 0; i < sz; i++) {
+                String newItem = msgs.get(i);
+                if (newItem.equals(oldNewest))
+                    break;
+                mData.add(i, newItem);
+            }
+            int newSz = mData.size();
+            for (int i = newSz - 1; i > MAX_LOG_LENGTH; i--) {
+                mData.remove(i);
+            }
+        }
+        return msgs;
+    }
+
+    @Override
+    public void deliverResult(List<String> 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<String> 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);
+        }
+
+        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;
+        }
+    }
+
+    @Override
+    public void onCanceled(List<String> 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<String> 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.
+    }
+}