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. + } +}