gui: Add ability to view detailed information about routes
Change-Id: Ib284082946287fd3f4927f83ab24f2f82b1021f2
diff --git a/app/src/main/java/net/named_data/nfd/MainActivity.java b/app/src/main/java/net/named_data/nfd/MainActivity.java
index 945165a..aecc7f9 100644
--- a/app/src/main/java/net/named_data/nfd/MainActivity.java
+++ b/app/src/main/java/net/named_data/nfd/MainActivity.java
@@ -29,6 +29,7 @@
import android.view.MenuItem;
import com.intel.jndn.management.types.FaceStatus;
+import com.intel.jndn.management.types.RibEntry;
import java.util.ArrayList;
@@ -38,7 +39,8 @@
public class MainActivity extends ActionBarActivity
implements DrawerFragment.DrawerCallbacks,
LogcatFragment.Callbacks,
- FaceListFragment.Callbacks
+ FaceListFragment.Callbacks,
+ RouteListFragment.Callbacks
{
@Override
@@ -174,6 +176,12 @@
replaceContentFragmentWithBackstack(FaceStatusFragment.newInstance(faceStatus));
}
+ @Override
+ public void onRouteItemSelected(RibEntry ribEntry)
+ {
+ replaceContentFragmentWithBackstack(RouteInfoFragment.newInstance(ribEntry));
+ }
+
//////////////////////////////////////////////////////////////////////////////
/** Reference to drawer fragment */
diff --git a/app/src/main/java/net/named_data/nfd/RouteInfoFragment.java b/app/src/main/java/net/named_data/nfd/RouteInfoFragment.java
new file mode 100644
index 0000000..bf22848
--- /dev/null
+++ b/app/src/main/java/net/named_data/nfd/RouteInfoFragment.java
@@ -0,0 +1,279 @@
+package net.named_data.nfd;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.ListFragment;
+import android.util.Pair;
+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 android.widget.Toast;
+
+import com.intel.jndn.management.types.FaceStatus;
+import com.intel.jndn.management.types.RibEntry;
+import com.intel.jndn.management.types.Route;
+
+import net.named_data.jndn.encoding.EncodingException;
+import net.named_data.jndn.util.Blob;
+import net.named_data.nfd.utils.G;
+import net.named_data.nfd.utils.Nfdc;
+
+import org.w3c.dom.Text;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RouteInfoFragment extends ListFragment {
+
+ public static RouteInfoFragment
+ newInstance(RibEntry ribEntry) {
+ Bundle args = new Bundle();
+ args.putByteArray(ROUTE_INFORMATION, ribEntry.wireEncode().getImmutableArray());
+
+ RouteInfoFragment fragment = new RouteInfoFragment();
+ fragment.setArguments(args);
+
+ return fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity)
+ {
+ super.onAttach(activity);
+ try {
+ m_callbacks = (FaceListFragment.Callbacks)activity;
+ } catch (Exception e) {
+ G.Log("Hosting activity must implement FaceListFragment.Callbacks: " + e);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ m_ribEntry = new RibEntry();
+ try {
+ m_ribEntry.wireDecode(new Blob(getArguments().getByteArray(ROUTE_INFORMATION)).buf());
+ }
+ catch (EncodingException e) {
+ G.Log("ROUTE_INFORMATION: EncodingException: " + e);
+ }
+
+ setListAdapter(new RouteFaceListAdapter(getActivity(), m_ribEntry));
+ }
+
+ @SuppressLint("InflateParams")
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState)
+ {
+ return inflater.inflate(R.layout.fragment_route_detail, null);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState)
+ {
+ super.onViewCreated(view, savedInstanceState);
+ TextView prefix = (TextView)view.findViewById(R.id.route_detail_prefix);
+ prefix.setText(m_ribEntry.getName().toUri());
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+ m_faceListAsyncTask = new FaceListAsyncTask();
+ m_faceListAsyncTask.execute();
+ }
+
+ @Override
+ public void onPause()
+ {
+ super.onPause();
+ if (m_faceListAsyncTask != null) {
+ m_faceListAsyncTask.cancel(false);
+ m_faceListAsyncTask = null;
+ }
+ }
+
+ @Override
+ public void
+ onListItemClick(ListView l, View v, int position, long id)
+ {
+ if (m_callbacks != null) {
+ RouteFaceListAdapter adapter = (RouteFaceListAdapter)l.getAdapter();
+ if (adapter.m_faces == null)
+ return;
+
+ Route route = adapter.getItem(position);
+ FaceStatus faceStatus = adapter.m_faces.get(route.getFaceId());
+ m_callbacks.onFaceItemSelected(faceStatus);
+ }
+ }
+
+ /////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Updates the underlying adapter with the given list of FaceStatus.
+ *
+ * Note: This method should only be called from the UI thread.
+ *
+ * @param list Update ListView with the given List<FaceStatus>
+ */
+ private void updateFaceList(List<FaceStatus> list) {
+ ((RouteFaceListAdapter)getListAdapter()).updateFaceList(list);
+ }
+
+
+ private static class RouteFaceListAdapter extends BaseAdapter {
+
+ public RouteFaceListAdapter(Context context, RibEntry ribEntry)
+ {
+ this.m_layoutInflater = LayoutInflater.from(context);
+ this.m_ribEntry = ribEntry;
+ }
+
+ private void
+ updateFaceList(List<FaceStatus> faces)
+ {
+ m_faces = new HashMap<>();
+ for (FaceStatus faceStatus : faces) {
+ m_faces.put(faceStatus.getFaceId(), faceStatus);
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int
+ getCount()
+ {
+ return m_ribEntry.getRoutes().size();
+ }
+
+ @Override
+ public Route
+ getItem(int position)
+ {
+ return m_ribEntry.getRoutes().get(position);
+ }
+
+ @Override
+ public long
+ getItemId(int position)
+ {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ListItemHolder holder;
+ if (convertView == null) {
+ holder = new ListItemHolder();
+
+ convertView = m_layoutInflater.inflate(R.layout.list_item_route_info_item, null);
+ convertView.setTag(holder);
+
+ holder.m_title = (TextView)convertView.findViewById(R.id.list_item_route_info_title);
+ holder.m_value = (TextView)convertView.findViewById(R.id.list_item_route_info_value);
+ } else {
+ holder = (ListItemHolder)convertView.getTag();
+ }
+
+ Route r = getItem(position);
+ String faceInfo = String.valueOf(r.getFaceId());
+
+ if (m_faces != null) {
+ FaceStatus status = m_faces.get(r.getFaceId());
+ faceInfo += " (" + status.getUri() + ")";
+ }
+
+ holder.m_title.setText(faceInfo);
+ holder.m_value.setText("origin: " + String.valueOf(r.getOrigin()) + " " +
+ "cost: " + String.valueOf(r.getCost()) + " " +
+ (r.getFlags().getChildInherit() ? "ChildInherit " : "") +
+ (r.getFlags().getCapture() ? "Capture " : "")
+ // +
+ // (r.getExpirationPeriod() > 0 ? "Expires in " + PeriodFormat.getDefault().print(new Period((int)(1000*r.getExpirationPeriod()))) : "")
+ );
+
+ return convertView;
+ }
+
+ private static class ListItemHolder {
+ private TextView m_title;
+ private TextView m_value;
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+ private LayoutInflater m_layoutInflater;
+ private Map<Integer, FaceStatus> m_faces;
+ private RibEntry m_ribEntry;
+ }
+
+ /**
+ * AsyncTask that gets the list of faces from the running NFD.
+ */
+ private class FaceListAsyncTask extends AsyncTask<Void, Void, Pair<List<FaceStatus>, Exception>> {
+ @Override
+ protected void
+ onPreExecute() {
+ }
+
+ @Override
+ protected Pair<List<FaceStatus>, Exception>
+ doInBackground(Void... params) {
+ Exception returnException = null;
+ Nfdc nfdc = new Nfdc();
+ List<FaceStatus> faceStatusList = null;
+ try {
+ faceStatusList = nfdc.faceList();
+ } catch (Exception e) {
+ returnException = e;
+ }
+ nfdc.shutdown();
+ return new Pair<>(faceStatusList, returnException);
+ }
+
+ @Override
+ protected void
+ onCancelled() {
+ // Nothing to do here; No change in UI.
+ }
+
+ @Override
+ protected void
+ onPostExecute(Pair<List<FaceStatus>, Exception> result) {
+ if (result.second != null) {
+ Toast.makeText(getActivity(),
+ "Error communicating with NFD (" + result.second.getMessage() + ")",
+ Toast.LENGTH_LONG).show();
+ }
+
+ updateFaceList(result.first);
+ }
+ }
+
+
+ /////////////////////////////////////////////////////////////////////////////
+
+ /** Bundle argument key for face information byte array */
+ private static final String ROUTE_INFORMATION = "net.named_data.nfd.route_information";
+
+ private RibEntry m_ribEntry;
+
+ /** Callback handler of the hosting activity */
+ private FaceListFragment.Callbacks m_callbacks;
+
+ /** Reference to the most recent AsyncTask that was created for listing faces */
+ private FaceListAsyncTask m_faceListAsyncTask;
+}
diff --git a/app/src/main/java/net/named_data/nfd/RouteListFragment.java b/app/src/main/java/net/named_data/nfd/RouteListFragment.java
index 61d39d5..f570545 100644
--- a/app/src/main/java/net/named_data/nfd/RouteListFragment.java
+++ b/app/src/main/java/net/named_data/nfd/RouteListFragment.java
@@ -19,6 +19,7 @@
package net.named_data.nfd;
+import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -30,15 +31,18 @@
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
+import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
+import com.intel.jndn.management.types.FaceStatus;
import com.intel.jndn.management.types.RibEntry;
import com.intel.jndn.management.types.Route;
import net.named_data.jndn.Name;
import net.named_data.jndn_xx.util.FaceUri;
+import net.named_data.nfd.utils.G;
import net.named_data.nfd.utils.Nfdc;
import java.util.ArrayList;
@@ -51,6 +55,27 @@
return new RouteListFragment();
}
+ public interface Callbacks {
+ /**
+ * This method is called when a route is selected and more
+ * information about it should be presented to the user.
+ *
+ * @param ribEntry RibEntry instance with information about the selected route
+ */
+ public void onRouteItemSelected(RibEntry ribEntry);
+ }
+
+ @Override
+ public void onAttach(Activity activity)
+ {
+ super.onAttach(activity);
+ try {
+ m_callbacks = (Callbacks)activity;
+ } catch (Exception e) {
+ G.Log("Hosting activity must implement this fragment's callbacks: " + e);
+ }
+ }
+
@Override
public void
onCreate(Bundle savedInstanceState) {
@@ -114,6 +139,15 @@
}
@Override
+ public void onListItemClick(ListView l, View v, int position, long id)
+ {
+ if (m_callbacks != null) {
+ RibEntry ribEntry = ((RouteListAdapter)l.getAdapter()).getItem(position);
+ m_callbacks.onRouteItemSelected(ribEntry);
+ }
+ }
+
+ @Override
public void
createRoute(Name prefix, String faceUri)
{
@@ -361,6 +395,9 @@
/////////////////////////////////////////////////////////////////////////////
+ /** Callback handler of the hosting activity */
+ private Callbacks m_callbacks;
+
/** Reference to the most recent AsyncTask that was created for listing routes */
private RouteListAsyncTask m_routeListAsyncTask;
diff --git a/app/src/main/res/layout/fragment_route_detail.xml b/app/src/main/res/layout/fragment_route_detail.xml
new file mode 100644
index 0000000..048b2c5
--- /dev/null
+++ b/app/src/main/res/layout/fragment_route_detail.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ style="@style/default_linear_layout_padding"
+ >
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/fragment_route_details_title"
+ style="?android:listSeparatorTextViewStyle"
+ />
+
+ <TextView style="@style/default_custom_white_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:id="@+id/route_detail_prefix"/>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/fragment_route_details_next_hops"
+ style="?android:listSeparatorTextViewStyle"
+ />
+
+ <include
+ layout="@android:layout/list_content"
+ />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_route_info_item.xml b/app/src/main/res/layout/list_item_route_info_item.xml
new file mode 100644
index 0000000..8c2057a
--- /dev/null
+++ b/app/src/main/res/layout/list_item_route_info_item.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/item_background_activated"
+ >
+
+ <TextView
+ android:id="@+id/list_item_route_info_title"
+ android:text="Title"
+ style="@style/two_row_item_title"
+ />
+
+ <TextView
+ android:id="@+id/list_item_route_info_value"
+ android:text="Value"
+ style="@style/two_row_item_secondary"
+ />
+
+</LinearLayout>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index dc86688..87899fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -54,4 +54,6 @@
<string name="face_add_dialog_create_face">Create face</string>
<string name="route_add_dialog_create_route">Create route</string>
+ <string name="fragment_route_details_title">Route Details</string>
+ <string name="fragment_route_details_next_hops">List of next hops</string>
</resources>