From 15275680e8850c3037a136ca0a86eb68daea703d Mon Sep 17 00:00:00 2001 From: str4d <str4d@mail.i2p> Date: Tue, 12 Nov 2013 01:10:29 +0000 Subject: [PATCH] First pass at migrating Addressbook to use Loaders Broken, addresses do not load into mAdapter after a tab change. --- res/layout/addressbook_list_item.xml | 1 + .../router/adapter/AddressEntryAdapter.java | 41 +++++ .../router/fragment/AddressbookFragment.java | 121 +++++++++----- .../android/router/loader/AddressEntry.java | 19 +++ .../router/loader/AddressEntryLoader.java | 153 ++++++++++++++++++ 5 files changed, 296 insertions(+), 39 deletions(-) create mode 100644 src/net/i2p/android/router/adapter/AddressEntryAdapter.java create mode 100644 src/net/i2p/android/router/loader/AddressEntry.java create mode 100644 src/net/i2p/android/router/loader/AddressEntryLoader.java diff --git a/res/layout/addressbook_list_item.xml b/res/layout/addressbook_list_item.xml index 7e058a0f9..a45d5e70c 100644 --- a/res/layout/addressbook_list_item.xml +++ b/res/layout/addressbook_list_item.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/text" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="6dp" diff --git a/src/net/i2p/android/router/adapter/AddressEntryAdapter.java b/src/net/i2p/android/router/adapter/AddressEntryAdapter.java new file mode 100644 index 000000000..e383d98f3 --- /dev/null +++ b/src/net/i2p/android/router/adapter/AddressEntryAdapter.java @@ -0,0 +1,41 @@ +package net.i2p.android.router.adapter; + +import java.util.List; + +import net.i2p.android.router.R; +import net.i2p.android.router.loader.AddressEntry; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +public class AddressEntryAdapter extends ArrayAdapter<AddressEntry> { + private final LayoutInflater mInflater; + + public AddressEntryAdapter(Context context) { + super(context, R.layout.addressbook_list_item); + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public void setData(List<AddressEntry> addresses) { + clear(); + if (addresses != null) { + for (AddressEntry address : addresses) { + add(address); + } + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v = mInflater.inflate(R.layout.addressbook_list_item, parent, false); + AddressEntry address = getItem(position); + + TextView text = (TextView) v.findViewById(R.id.text); + text.setText(address.getHostName()); + + return v; + } +} diff --git a/src/net/i2p/android/router/fragment/AddressbookFragment.java b/src/net/i2p/android/router/fragment/AddressbookFragment.java index 9f38a8135..333dff74a 100644 --- a/src/net/i2p/android/router/fragment/AddressbookFragment.java +++ b/src/net/i2p/android/router/fragment/AddressbookFragment.java @@ -5,29 +5,37 @@ import android.content.Context; 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.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; +import net.i2p.android.router.adapter.AddressEntryAdapter; +import net.i2p.android.router.fragment.I2PFragmentBase.RouterContextProvider; +import net.i2p.android.router.loader.AddressEntry; +import net.i2p.android.router.loader.AddressEntryLoader; +import net.i2p.router.RouterContext; -public class AddressbookFragment extends ListFragment { +public class AddressbookFragment extends ListFragment implements + LoaderManager.LoaderCallbacks<List<AddressEntry>> { + public static final String BOOK_NAME = "book_name"; + + private static final int ROUTER_LOADER_ID = 1; + private static final int PRIVATE_LOADER_ID = 2; + + RouterContextProvider mRouterContextProvider; OnAddressSelectedListener mCallback; - private ArrayAdapter<String> mAdapter; + private AddressEntryAdapter mAdapter; + private String mBook; // Container Activity must implement this interface public interface OnAddressSelectedListener { @@ -38,6 +46,15 @@ public class AddressbookFragment extends ListFragment { 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 { + mRouterContextProvider = (RouterContextProvider) activity; + } catch (ClassCastException e) { + throw new ClassCastException(activity.toString() + + " must implement RouterContextProvider"); + } + // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { @@ -58,39 +75,24 @@ public class AddressbookFragment extends ListFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); + mAdapter = new AddressEntryAdapter(getActivity()); + mBook = getArguments().getString(BOOK_NAME); - // 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(); + LoaderManager lm = getLoaderManager(); + // If the Router is running, or there is an existing Loader + if (getRouterContext() != null || lm.getLoader("private".equals(mBook) ? + PRIVATE_LOADER_ID : ROUTER_LOADER_ID) != null) { + setEmptyText("No hosts in address book " + mBook); + + setListShown(false); + lm.initLoader("private".equals(mBook) ? + PRIVATE_LOADER_ID : ROUTER_LOADER_ID, null, this); + } else { + setEmptyText(getResources().getString( + R.string.router_not_running)); + } } @Override @@ -128,4 +130,45 @@ public class AddressbookFragment extends ListFragment { public void filterAddresses(String query) { mAdapter.getFilter().filter(query); } + + // Duplicated from I2PFragmentBase because this extends ListFragment + private RouterContext getRouterContext() { + return mRouterContextProvider.getRouterContext(); + } + + // LoaderManager.LoaderCallbacks<List<AddressEntry>> + + public Loader<List<AddressEntry>> onCreateLoader(int id, Bundle args) { + return new AddressEntryLoader(getActivity(), + getRouterContext(), mBook); + } + + public void onLoadFinished(Loader<List<AddressEntry>> loader, + List<AddressEntry> data) { + if (loader.getId() == ("private".equals(mBook) ? + PRIVATE_LOADER_ID : ROUTER_LOADER_ID)) { + mAdapter.setData(data); + + if (isResumed()) { + setListShown(true); + } else { + setListShownNoAnimation(true); + } + + // Show Toast with addressbook size + int sz = data.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(); + } + } + + public void onLoaderReset(Loader<List<AddressEntry>> loader) { + if (loader.getId() == ("private".equals(mBook) ? + PRIVATE_LOADER_ID : ROUTER_LOADER_ID)) { + mAdapter.setData(null); + } + } } diff --git a/src/net/i2p/android/router/loader/AddressEntry.java b/src/net/i2p/android/router/loader/AddressEntry.java new file mode 100644 index 000000000..d48d48ff6 --- /dev/null +++ b/src/net/i2p/android/router/loader/AddressEntry.java @@ -0,0 +1,19 @@ +package net.i2p.android.router.loader; + +public class AddressEntry implements Comparable<Object> { + private final String mHostName; + + public AddressEntry(String hostName) { + mHostName = hostName; + } + + public String getHostName() { + return mHostName; + } + + public int compareTo(Object another) { + if (another instanceof AddressEntry) + return -1; + return mHostName.compareTo(((AddressEntry) another).getHostName()); + } +} diff --git a/src/net/i2p/android/router/loader/AddressEntryLoader.java b/src/net/i2p/android/router/loader/AddressEntryLoader.java new file mode 100644 index 000000000..9d0ae9fd1 --- /dev/null +++ b/src/net/i2p/android/router/loader/AddressEntryLoader.java @@ -0,0 +1,153 @@ +package net.i2p.android.router.loader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import net.i2p.android.router.util.Util; +import net.i2p.client.naming.NamingService; +import net.i2p.router.RouterContext; + +import android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +public class AddressEntryLoader extends AsyncTaskLoader<List<AddressEntry>> { + private static final String DEFAULT_NS = "BlockfileNamingService"; + private RouterContext mRContext; + private String mBook; + private List<AddressEntry> mData; + + public AddressEntryLoader(Context context, RouterContext rContext, String book) { + super(context); + mRContext = rContext; + mBook = book; + } + + @Override + public List<AddressEntry> loadInBackground() { + // get the names + NamingService ns = getNamingService(); + Util.i("NamingService: " + ns.getName()); + // After router shutdown we get nothing... why? + List<AddressEntry> ret = new ArrayList<AddressEntry>(); + for (String hostName : ns.getNames()) { + AddressEntry name = new AddressEntry(hostName); + ret.add(name); + } + Collections.sort(ret); + return ret; + } + + /** @return the NamingService for the current file name, or the root NamingService */ + private NamingService getNamingService() + { + NamingService root = mRContext.namingService(); + NamingService rv = searchNamingService(root, mBook); + return rv != null ? rv : root; + } + + /** depth-first search */ + private static NamingService searchNamingService(NamingService ns, String srch) + { + String name = ns.getName(); + if (name.equals(srch) || basename(name).equals(srch) || name.equals(DEFAULT_NS)) + return ns; + List<NamingService> list = ns.getNamingServices(); + if (list != null) { + for (NamingService nss : list) { + NamingService rv = searchNamingService(nss, srch); + if (rv != null) + return rv; + } + } + return null; + } + + private static String basename(String filename) { + int slash = filename.lastIndexOf('/'); + if (slash >= 0) + filename = filename.substring(slash + 1); + return filename; + } + + @Override + public void deliverResult(List<AddressEntry> 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<AddressEntry> 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<AddressEntry> 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<AddressEntry> 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. + } +} -- GitLab