+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..0f2d5b5
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 16
+ buildToolsVersion "21.1.2"
+
+ defaultConfig {
+ applicationId "ch.benediktkoeppel.code.droidplane"
+ minSdkVersion 8
+ targetSdkVersion 17
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+}
+
+dependencies {
+ compile 'com.android.support:support-v4:18.0.0'
+ compile files('libs/acra-4.4.0.jar')
+ compile files('libs/libGoogleAnalyticsV2.jar')
+}
diff --git a/app/libs/acra-4.4.0.jar b/app/libs/acra-4.4.0.jar
new file mode 100644
index 0000000..1df9bfd
Binary files /dev/null and b/app/libs/acra-4.4.0.jar differ
diff --git a/app/libs/libGoogleAnalyticsV2.jar b/app/libs/libGoogleAnalyticsV2.jar
new file mode 100644
index 0000000..46d0e44
Binary files /dev/null and b/app/libs/libGoogleAnalyticsV2.jar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6d14e8e
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java
new file mode 100644
index 0000000..32e1e60
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/HorizontalMindmapView.java
@@ -0,0 +1,582 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import java.util.ArrayList;
+
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+public class HorizontalMindmapView extends HorizontalScrollView implements OnTouchListener, OnItemClickListener/*, OnItemLongClickListener*/ {
+
+ /**
+ * HorizontalScrollView can only have one view, so we need to add a
+ * LinearLayout underneath it, and then stuff all NodeColumns into this
+ * linearLayout.
+ */
+ private LinearLayout linearLayout;
+
+ /**
+ * nodeColumns holds the list of columns that are displayed in this
+ * HorizontalScrollView.
+ */
+ private ArrayList nodeColumns;
+
+ /**
+ * Gesture detector
+ */
+ private GestureDetector gestureDetector;
+
+ private HorizontalMindmapViewGestureDetector horizontalMindmapViewGestureDetector;
+
+ /**
+ * constants to determine the minimum swipe distance and speed
+ */
+ private static final int SWIPE_THRESHOLD_VELOCITY = 300;
+
+ /**
+ * Setting up a HorizontalMindmapView. We initialize the nodeColumns, define
+ * the layout parameters for the HorizontalScrollView and create the
+ * LinearLayout view inside the HorizontalScrollView.
+ * @param context the Application Context
+ */
+ public HorizontalMindmapView(Context context) {
+ super(context);
+
+ // list where all columns are stored
+ nodeColumns = new ArrayList();
+
+ // set the layout for the HorizontalScrollView itself
+ setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+
+ // create the layout parameters for a new LinearLayout
+ int height = LayoutParams.MATCH_PARENT;
+ int width = LayoutParams.MATCH_PARENT;
+ ViewGroup.LayoutParams linearLayoutParams = new ViewGroup.LayoutParams(width, height);
+
+ // create a LinearLayout in this HorizontalScrollView. All NodeColumns will go into that LinearLayout.
+ linearLayout = new LinearLayout(context);
+ linearLayout.setLayoutParams(linearLayoutParams);
+ this.addView(linearLayout);
+
+ // add a new gesture controller
+ horizontalMindmapViewGestureDetector = new HorizontalMindmapViewGestureDetector();
+ gestureDetector = new GestureDetector(getContext(), horizontalMindmapViewGestureDetector);
+
+ // register HorizontalMindmapView to receive all touch events on itself
+ setOnTouchListener(this);
+ }
+
+ /**
+ * Add a new NodeColumn to the HorizontalMindmapView
+ * @param nodeColumn the NodeColumn to add to the HorizontalMindmapView
+ */
+ public void addColumn(NodeColumn nodeColumn) {
+
+ // add the column to the layout
+ nodeColumns.add(nodeColumn);
+ linearLayout.addView(nodeColumn, linearLayout.getChildCount());
+ Log.d(MainApplication.TAG, "linearLayout now has " + linearLayout.getChildCount() + " items");
+
+ // register as onItemClickListener and onItemLongClickListener
+ nodeColumn.setOnItemClickListener(this);
+ }
+
+ /**
+ * GUI Helper to scroll the HorizontalMindmapView all the way to the right.
+ * Should be called after adding a NodeColumn.
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ public void scrollToRight() {
+
+ // a runnable that knows "this"
+ final class HorizontalMindmapViewRunnable implements Runnable {
+ HorizontalMindmapView horizontalMindmapView;
+
+ public HorizontalMindmapViewRunnable(HorizontalMindmapView horizontalMindmapView) {
+ this.horizontalMindmapView = horizontalMindmapView;
+ }
+
+ @Override
+ public void run() {
+ horizontalMindmapView.fullScroll(FOCUS_RIGHT);
+ }
+ }
+
+ new Handler().postDelayed(new HorizontalMindmapViewRunnable(this), 100L);
+ }
+
+ /**
+ * Removes all columns from this HorizontalMindmapView
+ */
+ public void removeAllColumns() {
+ nodeColumns.clear();
+ linearLayout.removeAllViews();
+ }
+
+ /**
+ * Adjusts the width of all columns in the HorizontalMindmapView
+ * @param columnWidth the width of each column
+ */
+ public void resizeAllColumns() {
+ for (NodeColumn nodeColumn : nodeColumns) {
+ nodeColumn.resizeColumnWidth();
+ }
+ }
+
+ /**
+ * Removes the rightmost column and returns true. If there was no column to
+ * remove, returns false. It never removes the last column, i.e. it never
+ * removes the root node of the mind map.
+ * @return True if a column was removed, false if no column was removed.
+ */
+ public boolean removeRightmostColumn() {
+
+ // only remove a column if we have at least 2 columns. If there is only
+ // one column, it will not be removed.
+ if ( nodeColumns.size() >= 2 ) {
+
+ // the column to remove
+ NodeColumn rightmostColumn = nodeColumns.get(nodeColumns.size()-1);
+
+ // remove it from the linear layout
+ linearLayout.removeView(rightmostColumn);
+
+ // remove it from the nodeColumns list
+ nodeColumns.remove(nodeColumns.size()-1);
+
+ // then deselect all nodes on the now newly rightmost column and let the column redraw
+ nodeColumns.get(nodeColumns.size()-1).deselectAllNodes();
+
+ // a column was removed, so we return true
+ return true;
+ }
+
+ // no column was removed, so we return false
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the number of columns in the HorizontalMindmapView.
+ * @return
+ */
+ public int getNumberOfColumns() {
+ return nodeColumns.size();
+ }
+
+ /**
+ * Returns the title of the parent node of the rightmost column. This is the
+ * same as the node name of the selected node from the 2nd-rightmost column.
+ * So this is the last node that the user has clicked.
+ * If the rightmost column has no parent, an empty string is returned.
+ *
+ * @return Title of the right most parent node or an empty string.
+ */
+ public String getTitleOfRightmostParent() {
+
+ if ( !nodeColumns.isEmpty() ) {
+
+ MindmapNode parent = nodeColumns.get(nodeColumns.size()-1).getParentNode();
+ return parent.text;
+
+ }
+
+ // there were no columns
+ else {
+ Log.d(MainApplication.TAG, "getTitleOfRightmostParent returned \"\" because nodeColumns is empty");
+ return "";
+ }
+ }
+
+ /**
+ * Remove all columns at the right of the specified column.
+ * @param nodeColumn
+ */
+ public void removeAllColumnsRightOf(NodeColumn nodeColumn) {
+
+ // we go from right to left, from the end of nodeColumns back to one
+ // element after nodeColumn
+ //
+ // nodeColumns = [ col1, col2, col3, col4, col5 ];
+ // removeAllColumnsRightOf(col2) will do:
+ // nodeColumns.size()-1 => 4
+ // nodeColumns.lastIndexOf(col2)+1 => 2
+ //
+ // for i in (4, 3, 2): remove rightmost column
+ // i = 4: remove col5
+ // i = 3: remove col4
+ // i = 2: remove col3
+ //
+ // so at the end, we have
+ // nodeColumns = [ col1, col2 ];
+ for (int i = nodeColumns.size()-1; i >= nodeColumns.lastIndexOf(nodeColumn)+1; i--) {
+
+ // remove this column
+ removeRightmostColumn();
+ }
+ }
+
+ /**
+ * navigates to the top of the Mindmap
+ */
+ public void top() {
+
+ // remove all ListView layouts in linearLayout parent_list_view
+ removeAllColumns();
+
+ // go down into the root node
+ down(MainApplication.getInstance().mindmap.getRootNode());
+ }
+
+ /**
+ * navigates back up one level in the Mindmap, if possible (otherwise does
+ * nothing)
+ */
+ public void up() {
+ up(false);
+ }
+
+ /**
+ * navigates back up one level in the Mindmap. If we already display the
+ * root node, the application will finish
+ */
+ public void upOrClose() {
+ up(true);
+ }
+
+ /**
+ * navigates back up one level in the Mindmap, if possible. If force is
+ * true, the application closes if we can't go further up
+ *
+ * @param force
+ */
+ public void up(boolean force) {
+
+ boolean wasColumnRemoved = removeRightmostColumn();
+
+ // close the application if no column was removed, and the force switch
+ // was on
+ if (!wasColumnRemoved && force) {
+ MainApplication.getMainActivityInstance().finish();
+ }
+
+ // enable the up navigation with the Home (app) button (top left corner)
+ enableHomeButtonIfEnoughColumns();
+
+ // get the title of the parent of the rightmost column (i.e. the
+ // selected node in the 2nd-rightmost column)
+ setApplicationTitle();
+
+ }
+
+ /**
+ * open up Node node, and display all its child nodes
+ *
+ * @param node
+ */
+ public void down(MindmapNode node) {
+
+ // add a new column for this node and add it to the
+ // HorizontalMindmapView
+ NodeColumn nodeColumn = new NodeColumn(getContext(), node);
+ addColumn(nodeColumn);
+
+ // then scroll all the way to the right
+ scrollToRight();
+
+ // enable the up navigation with the Home (app) button (top left corner)
+ enableHomeButtonIfEnoughColumns();
+
+ // get the title of the parent of the rightmost column (i.e. the
+ // selected node in the 2nd-rightmost column)
+ setApplicationTitle();
+
+ }
+
+ /**
+ * Sets the application title to the name of the parent node of the
+ * rightmost column, which is the most recently clicked node.
+ */
+ void setApplicationTitle() {
+ // get the title of the parent of the rightmost column (i.e. the
+ // selected node in the 2nd-rightmost column)
+ // set the application title to this nodeTitle. If the nodeTitle is
+ // empty, we set the default Application title
+ String nodeTitle = getTitleOfRightmostParent();
+ Log.d(MainApplication.TAG, "nodeTitle = " + nodeTitle);
+ if (nodeTitle.equals("")) {
+ Log.d(MainApplication.TAG, "Setting application title to default string: " + getResources().getString(R.string.app_name));
+ MainApplication.getMainActivityInstance().setTitle(R.string.app_name);
+ } else {
+ Log.d(MainApplication.TAG,"Setting application title to node name: " + nodeTitle);
+ MainApplication.getMainActivityInstance().setTitle(nodeTitle);
+ }
+ }
+
+ /**
+ * Enables the Home button in the application if we have enough columns,
+ * i.e. if "Up" will remove a column.
+ */
+ void enableHomeButtonIfEnoughColumns() {
+ // if we only have one column (i.e. this is the root node), then we
+ // disable the home button
+ int numberOfColumns = getNumberOfColumns();
+ if (numberOfColumns >= 2) {
+ MainApplication.getMainActivityInstance().enableHomeButton();
+ } else {
+ MainApplication.getMainActivityInstance().disableHomeButton();
+ }
+ }
+
+
+
+ /* (non-Javadoc)
+ * Handler when one of the ListItem's item is clicked
+ * Find the node which was clicked, and redraw the screen with this node as new parent
+ * if the clicked node has no child, then we stop here
+ * @see android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget.AdapterView, android.view.View, int, long)
+ */
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+
+ // the clicked column
+ // parent is the ListView in which the user clicked. Because
+ // NodeColumn does not extend ListView (it only wraps a ListView), we
+ // have to find out which NodeColumn it was. We can do so because
+ // NodeColumn.getNodeColumnFromListView uses a static HashMap to do the
+ // translation.
+ NodeColumn clickedNodeColumn = NodeColumn.getNodeColumnFromListView((ListView)parent);
+
+ // remove all columns right of the column which was clicked
+ removeAllColumnsRightOf(clickedNodeColumn);
+
+ // then get the clicked node
+ MindmapNode clickedNode = clickedNodeColumn.getNodeAtPosition(position);
+
+ // if the clicked node has child nodes, we set it to selected and drill down
+ if ( clickedNode.getNumChildMindmapNodes() > 0 ) {
+
+ // give it a special color
+ clickedNodeColumn.setItemColor(position);
+
+ // and drill down
+ down(clickedNode);
+ }
+
+ // otherwise (no children) then we just update the application title to the new parent node
+ else {
+ setApplicationTitle();
+ }
+ }
+
+
+ /*
+ * (non-Javadoc)
+ * Will be called whenever the HorizontalScrollView is
+ * touched. We have to capture the move left and right events here, and snap
+ * to the appropriate column borders.
+ *
+ * @see android.view.View.OnTouchListener#onTouch(android.view.View,
+ * android.view.MotionEvent)
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+
+ // first, we let the gestureDetector examine the event. It will process
+ // the event if it was a gesture, i.e. if it was fast enough to trigger
+ // a Fling. If it handled the event, we don't process it further.
+ // This gesture can be triggered if the user moves the finger fast
+ // enough. He does not necessarily have to move so far that the next
+ // column is mostly visible.
+ if ( gestureDetector.onTouchEvent(event) ) {
+ Log.d(MainApplication.TAG, "Touch event was processed by HorizontalMindmapView (gesture)");
+ return true;
+ }
+
+ // If it was not a gesture (i.e. the user moved his finger too slow), we
+ // simply snap to the next closest column border.
+ else if ( event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL ) {
+
+ // now we need to find out where the HorizontalMindmapView is horizontally scrolled
+ int scrollX = getScrollX();
+ Log.d(MainApplication.TAG, "HorizontalMindmapView is scrolled horizontally to " + scrollX);
+
+ // get the leftmost column that is still (partially) visible
+ NodeColumn leftmostVisibleColumn = getLeftmostVisibleColumn();
+
+ // get the number of visible pixels of this column
+ int numVisiblePixelsOnColumn = getVisiblePixelOfLeftmostColumn();
+
+ // if we couldn't find a column, we could not process this event. I'm not sure how this might ever happen
+ if ( leftmostVisibleColumn == null ) {
+ Log.e(MainApplication.TAG, "No leftmost visible column was detected. Not sure how this could happen!");
+ return false;
+ }
+
+ // and then determine if the leftmost visible column shows more than 50% of its full width
+ // if it shows more than 50%, then we scroll to the left, so that we can see it fully
+ if ( numVisiblePixelsOnColumn < leftmostVisibleColumn.getWidth()/2 ) {
+ Log.d(MainApplication.TAG, "Scrolling to the left, so that we can see the column fully");
+ smoothScrollTo(scrollX + numVisiblePixelsOnColumn, 0);
+ }
+
+ // if it shows less than 50%, then we scroll to the right, so that is not visible anymore
+ else {
+ Log.d(MainApplication.TAG, "Scrolling to the right, so that the column is not visible anymore");
+ smoothScrollTo(scrollX + numVisiblePixelsOnColumn - leftmostVisibleColumn.getWidth(), 0);
+ }
+
+ // we have processed this event
+ Log.d(MainApplication.TAG, "Touch event was processed by HorizontalMindmapView (no gesture)");
+ return true;
+
+ }
+
+ // if we did not process the event ourself we let the caller know
+ else {
+ Log.d(MainApplication.TAG, "Touch event was not processed by HorizontalMindmapView");
+ return false;
+ }
+ }
+
+ /**
+ * Get the column at the left edge of the screen.
+ * @return NodeColumn
+ */
+ private NodeColumn getLeftmostVisibleColumn() {
+
+ // how much we are horizontally scrolled
+ int scrollX = getScrollX();
+
+ // how many columns fit into less than scrollX space? as soon as
+ // sumColumnWdiths > scrollX, we have just added the first visible
+ // column at the left.
+ int sumColumnWidths = 0;
+ NodeColumn leftmostVisibleColumn = null;
+ for (int i = 0; i < nodeColumns.size(); i++) {
+ sumColumnWidths += nodeColumns.get(i).getWidth();
+
+ // if the sum of all columns so far exceeds scrollX, the current NodeColumn is (at least a little bit) visible
+ if (sumColumnWidths >= scrollX) {
+ leftmostVisibleColumn = nodeColumns.get(i);
+ break;
+ }
+ }
+
+ return leftmostVisibleColumn;
+ }
+
+ /**
+ * Get the number of pixels that are visible on the leftmost column.
+ * @return
+ */
+ private int getVisiblePixelOfLeftmostColumn() {
+
+ // how much we are horizontally scrolled
+ int scrollX = getScrollX();
+
+ // how many columns fit into less than scrollX space? as soon as
+ // sumColumnWdiths > scrollX, we have just added the first visible
+ // column at the left.
+ int sumColumnWidths = 0;
+ int numVisiblePixelsOnColumn = 0;
+ for (int i = 0; i < nodeColumns.size(); i++) {
+ sumColumnWidths += nodeColumns.get(i).getWidth();
+
+ // if the sum of all columns so far exceeds scrollX, the current NodeColumn is (at least a little bit) visible
+ if (sumColumnWidths >= scrollX) {
+ // how many pixels are visible of this column?
+ numVisiblePixelsOnColumn = sumColumnWidths - scrollX;
+ break;
+ }
+ }
+
+ return numVisiblePixelsOnColumn;
+ }
+
+ /**
+ * The HorizontalMindmapViewGestureDetector should detect the onFling event.
+ * However, it never receives the onDown event, so when it gets the onFling
+ * the event1 is empty, and we can't detect the fling properly.
+ */
+ class HorizontalMindmapViewGestureDetector extends SimpleOnGestureListener {
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ /*
+ * (non-Javadoc) onFling is called whenever a Fling (a fast swipe) event
+ * is detected. However, for some reason, our onDown method is never
+ * called, and the onFling method never gets a valid event1 (it's always
+ * null). So instead of relying on event1 and event2 (and determine the
+ * distance the finger moved), we only consider the velocity of the
+ * fling. This is not as accurate as it could be, but it works.
+ *
+ * @see
+ * android.view.GestureDetector.SimpleOnGestureListener#onFling(android
+ * .view.MotionEvent, android.view.MotionEvent, float, float)
+ */
+ @Override
+ public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {
+
+ try {
+
+ // how much we are horizontally scrolled
+ int scrollX = getScrollX();
+ Log.d(MainApplication.TAG, "Velocity = " + velocityX);
+
+ // get the leftmost column that is still (partially) visible
+ NodeColumn leftmostVisibleColumn = getLeftmostVisibleColumn();
+
+ // get the number of visible pixels of this column
+ int numVisiblePixelsOnColumn = getVisiblePixelOfLeftmostColumn();
+
+ // if we have moved at least the SWIPE_MIN_DISTANCE to the right and at faster than SWIPE_THRESHOLD_VELOCITY
+ if (velocityX < 0 && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
+
+ // scroll to the target column
+ smoothScrollTo(scrollX + numVisiblePixelsOnColumn, 0);
+
+ Log.d(MainApplication.TAG, "processed the Fling to Right gesture");
+ return true;
+ }
+
+ // the same as above but from to the left
+ else if ( velocityX > 0 && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) {
+
+ // scroll to the target column
+ // scrolls in the wrong direction
+ smoothScrollTo(scrollX + numVisiblePixelsOnColumn - leftmostVisibleColumn.getWidth(), 0);
+
+ Log.d(MainApplication.TAG, "processed the Fling to Left gesture");
+ return true;
+ }
+
+ // we did not process this gesture
+ else {
+ Log.d(MainApplication.TAG, "Fling was no real fling");
+ return false;
+ }
+
+ } catch (Exception e) {
+ Log.d(MainApplication.TAG, "A whole lot of stuff could have gone wrong here");
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainActivity.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainActivity.java
new file mode 100644
index 0000000..977cf4f
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainActivity.java
@@ -0,0 +1,390 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import org.acra.ACRA;
+
+import android.annotation.SuppressLint;
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.webkit.MimeTypeMap;
+import android.widget.AdapterView;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.google.analytics.tracking.android.EasyTracker;
+
+/**
+ * The MainActivity can be started from the App Launcher, or with a File Open
+ * intent. If the MainApplication was already running, the previously used
+ * document is re-used. Also, most of the information about the mind map and the
+ * currently opened views is stored in the MainApplication. This enables the
+ * MainActivity to resume wherever it was before it got restarted. A restart
+ * can happen when the screen is rotated, and we want to continue wherever we
+ * were before the screen rotate.
+ */
+public class MainActivity extends Activity {
+
+ MainApplication application;
+
+ public final static String INTENT_START_HELP = "ch.benediktkoeppel.code.droidplane.INTENT_START_HELP";
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EasyTracker.getInstance().activityStart(this);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ EasyTracker.getInstance().activityStop(this);
+ EasyTracker.getInstance().dispatch();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ application = (MainApplication)getApplication();
+ MainApplication.setMainActivityInstance(this);
+
+ // initialize android stuff
+ // EasyTracker
+ EasyTracker.getInstance().setContext(this);
+ EasyTracker.getTracker().sendView("MainActivity");
+
+ // enable the Android home button
+ enableHomeButton();
+
+ // intents (how we are called)
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ String type = intent.getType();
+
+ // if the application was reset, or the document has changed, we need to re-initialize everything
+ if ( application.mindmap == null || application.mindmap.document == null
+ || (application.mindmap.getUri()!=intent.getData() && intent.getData()!=null)
+ || (intent.getBooleanExtra(INTENT_START_HELP,false))
+ ) {
+
+ // create a new Mindmap
+ application.mindmap = new Mindmap();
+
+ // create a new HorizontalMindmapView
+ application.horizontalMindmapView = new HorizontalMindmapView(getApplicationContext());
+
+ // prepare loading of the Mindmap file
+ InputStream mm = null;
+
+ // determine whether we are started from the EDIT or VIEW intent, or whether we are started from the launcher
+ // started from ACTION_EDIT/VIEW intent
+ if ((Intent.ACTION_EDIT.equals(action)||Intent.ACTION_VIEW.equals(action)) && type != null) {
+
+ Log.d(MainApplication.TAG, "started from ACTION_EDIT/VIEW intent");
+
+ // get the URI to the target document (the Mindmap we are opening) and open the InputStream
+ Uri uri = intent.getData();
+ if ( uri != null ) {
+ ContentResolver cr = getContentResolver();
+ try {
+ mm = cr.openInputStream(uri);
+ } catch (FileNotFoundException e) {
+
+ abortWithPopup(R.string.filenotfound);
+
+ ACRA.getErrorReporter().putCustomData("Exception", "FileNotFoundException");
+ ACRA.getErrorReporter().putCustomData("Intent", "ACTION_EDIT/VIEW");
+ ACRA.getErrorReporter().putCustomData("URI", uri.toString());
+ e.printStackTrace();
+ }
+ } else {
+ abortWithPopup(R.string.novalidfile);
+ }
+
+ // store the Uri. Next time the MainActivity is started, we'll
+ // check whether the Uri has changed (-> load new document) or
+ // remained the same (-> reuse previous document)
+ application.mindmap.setUri(uri);
+ }
+
+ // started from the launcher
+ else {
+ Log.d(MainApplication.TAG, "started from app launcher intent");
+
+ // display the default Mindmap "example.mm", from the resources
+ mm = getApplicationContext().getResources().openRawResource(R.raw.example);
+ }
+
+ // load the mindmap
+ Log.d(MainApplication.TAG, "InputStream fetched, now starting to load document");
+ application.mindmap.loadDocument(mm);
+
+ // add the HorizontalMindmapView to the Layout Wrapper
+ ((LinearLayout)findViewById(R.id.layout_wrapper)).addView(application.horizontalMindmapView);
+
+ // navigate down into the root node
+ application.horizontalMindmapView.down(application.mindmap.getRootNode());
+ }
+
+ // otherwise, we can display the existing HorizontalMindmapView again
+ else {
+
+ // add the HorizontalMindmapView to the Layout Wrapper
+ LinearLayout tmp_parent = ((LinearLayout)application.horizontalMindmapView.getParent());
+ if ( tmp_parent != null ) {
+ tmp_parent.removeView(application.horizontalMindmapView);
+ }
+ ((LinearLayout)findViewById(R.id.layout_wrapper)).addView(application.horizontalMindmapView);
+
+ // fix the widths of all columns
+ application.horizontalMindmapView.resizeAllColumns();
+
+ // and then scroll to the right
+ application.horizontalMindmapView.scrollToRight();
+
+ // enable the up navigation with the Home (app) button (top left corner)
+ application.horizontalMindmapView.enableHomeButtonIfEnoughColumns();
+
+ // get the title of the parent of the rightmost column (i.e. the selected node in the 2nd-rightmost column)
+ application.horizontalMindmapView.setApplicationTitle();
+ }
+ }
+
+
+
+
+ /**
+ * enables the home button if the Android version allows it
+ */
+ @SuppressLint("NewApi") void enableHomeButton() {
+ // menu bar: if we are at least at API 11, the Home button is kind of a back button in the app
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ActionBar bar = getActionBar();
+ bar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ /**
+ * disables the home button if the Android version allows it
+ */
+ @SuppressLint("NewApi") void disableHomeButton() {
+ // menu bar: if we are at least at API 11, the Home button is kind of a back button in the app
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ActionBar bar = getActionBar();
+ bar.setDisplayHomeAsUpEnabled(false);
+ }
+ }
+
+
+
+ /* (non-Javadoc)
+ * Creates the options menu
+ * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu)
+ */
+ @Override
+ public boolean onCreateOptionsMenu(android.view.Menu menu) {
+
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main, menu);
+ return true;
+ }
+
+
+ /* (non-Javadoc)
+ * Handler for the back button
+ * Navigate one level up, and stay at the root node
+ * @see android.app.Activity#onBackPressed()
+ */
+ @Override
+ public void onBackPressed() {
+ application.horizontalMindmapView.upOrClose();
+ }
+
+ /*
+ * (non-Javadoc) Handler of all menu events Home button: navigate one level
+ * up, and exit the application if the home button is pressed at the root
+ * node Menu Up: navigate one level up, and stay at the root node
+ *
+ * @see android.app.Activity#onOptionsItemSelected(android.view.MenuItem)
+ */
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+
+ switch (item.getItemId()) {
+
+ // "Up" menu action
+ case R.id.up:
+ application.horizontalMindmapView.up();
+ break;
+
+ // "Top" menu action
+ case R.id.top:
+ application.horizontalMindmapView.top();
+ break;
+
+ // "Help" menu action
+ case R.id.help:
+
+ // create a new intent (without URI)
+ Intent helpIntent = new Intent(this, MainActivity.class);
+ helpIntent.putExtra(INTENT_START_HELP, true);
+ startActivity(helpIntent);
+
+ // App button (top left corner)
+ case android.R.id.home:
+ application.horizontalMindmapView.up();
+ break;
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * It looks like the onContextItemSelected has to be overwritten in a class
+ * extending Activity. It was not possible to have this callback in the
+ * NodeColumn. As a result, we have to find out here again where the event happened
+ *
+ * @see android.app.Activity#onContextItemSelected(android.view.MenuItem)
+ */
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+
+ AdapterView.AdapterContextMenuInfo contextMenuInfo = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
+
+ // contextMenuInfo.position is the position in the ListView where the
+ // context menu was loaded, i.e. the index of the item in our
+ // mindmapNodes list
+
+ // MindmapNode extends LinearView, so we can cast targetView back to MindmapNode
+ MindmapNode mindmapNode = (MindmapNode)contextMenuInfo.targetView;
+ Log.d(MainApplication.TAG, "mindmapNode.text = " + mindmapNode.text);
+
+ Log.d(MainApplication.TAG, "contextMenuInfo.position = " + contextMenuInfo.position);
+ Log.d(MainApplication.TAG, "item.getTitle() = " + item.getTitle());
+
+ switch (item.getItemId()) {
+
+ // copy node text to clipboard
+ case R.id.contextcopy:
+ Log.d(MainApplication.TAG, "Copying text to clipboard");
+ ClipboardManager clipboardManager = (ClipboardManager)getSystemService(CLIPBOARD_SERVICE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ ClipData clipData = ClipData.newPlainText("node", mindmapNode.text);
+ clipboardManager.setPrimaryClip(clipData);
+ } else {
+ clipboardManager.setText(mindmapNode.text);
+ }
+
+ break;
+
+ // open the URI specified in the "LINK" tag
+ case R.id.contextopenlink:
+ Log.d(MainApplication.TAG, "Opening node link " + mindmapNode.link);
+
+ // try opening the link normally with an intent
+ try {
+ Intent openUriIntent = new Intent(Intent.ACTION_VIEW);
+ openUriIntent.setData(mindmapNode.link);
+ startActivity(openUriIntent);
+ break;
+ } catch (ActivityNotFoundException e) {
+ Log.d(MainApplication.TAG, "ActivityNotFoundException when opening link as normal intent");
+ }
+
+ // try to prepend file:// to open it as a file link
+ try {
+ // get path of mindmap file
+ String fileName = "no file";
+ if (mindmapNode.link.getPath().startsWith("/")) {
+ // absolute filename
+ fileName = mindmapNode.link.getPath();
+ } else {
+
+ // link is relative to mindmap file
+ String mindmapPath = application.mindmap.getUri().getPath();
+ Log.d(MainApplication.TAG, "Mindmap path " + mindmapPath);
+ String mindmapDirectoryPath = mindmapPath.substring(0, mindmapPath.lastIndexOf("/"));
+ Log.d(MainApplication.TAG, "Mindmap directory path " + mindmapDirectoryPath);
+ fileName = mindmapDirectoryPath + "/" + mindmapNode.link.getPath();
+
+ }
+ File file = new File(fileName);
+ if (!file.exists()) {
+ Toast.makeText(this, "File " + fileName + " does not exist.", Toast.LENGTH_SHORT).show();
+ Log.d(MainApplication.TAG, "File " + fileName + " does not exist.");
+ break;
+ }
+ if (!file.canRead()) {
+ Toast.makeText(this, "Can not read file " + fileName + ".", Toast.LENGTH_SHORT).show();
+ Log.d(MainApplication.TAG, "Can not read file " + fileName + ".");
+ break;
+ }
+ Log.d(MainApplication.TAG, "Opening file " + Uri.fromFile(file));
+ // http://stackoverflow.com/a/3571239/1067124
+ String extension = "";
+ int i = fileName.lastIndexOf('.');
+ int p = fileName.lastIndexOf('/');
+ if (i > p) {
+ extension = fileName.substring(i+1);
+ }
+ String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.fromFile(file), mime);
+ startActivity(intent);
+ } catch (Exception e1) {
+ Toast.makeText(this, "No application found to open " + mindmapNode.link, Toast.LENGTH_SHORT).show();
+ e1.printStackTrace();
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ /**
+ * Shows a popup with an error message and then closes the application
+ * @param stringResourceId
+ */
+ public void abortWithPopup(int stringResourceId) {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(stringResourceId);
+ builder.setCancelable(true);
+ builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainApplication.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainApplication.java
new file mode 100644
index 0000000..36b29fe
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MainApplication.java
@@ -0,0 +1,91 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import org.acra.ACRA;
+import org.acra.annotation.ReportsCrashes;
+
+import android.app.Application;
+import android.content.Context;
+
+/**
+ * The DroidPlane main application. It stores the loaded Uri and document, so
+ * that we can recreate the MainActivity after a screen rotation.
+ */
+@ReportsCrashes(formKey = "dE1VQVpQN2FNTWlLQXg1UUQ1b1VSN3c6MQ")
+public class MainApplication extends Application {
+
+ /**
+ * Android Logging TAG
+ */
+ public static final String TAG = "DroidPlane";
+
+ /**
+ * HorizontalMindmapView that contains all NodeColumns
+ */
+ public HorizontalMindmapView horizontalMindmapView;
+
+ /**
+ * the application context
+ */
+ private static Context context;
+
+ /**
+ * the main application instance
+ */
+ private static MainApplication instance;
+
+ /**
+ * the main activity instance
+ */
+ private static MainActivity mainActivityInstance;
+
+ /**
+ * A reference to the Mindmap document
+ */
+ public Mindmap mindmap;
+
+
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // initialize ACRA crash reports
+ ACRA.init(this);
+
+ // save the context
+ MainApplication.context = getApplicationContext();
+
+ // save the instance
+ MainApplication.instance = this;
+ }
+
+ /**
+ * Helper to return the application context, even for static methods.
+ * @return
+ */
+ public static Context getStaticApplicationContext() {
+ return MainApplication.context;
+ }
+
+ /**
+ * Get the main application instance
+ * @return the main application
+ */
+ public static MainApplication getInstance() {
+ return MainApplication.instance;
+ }
+
+ /**
+ * Stores the MainActivity instance
+ * @param mainActivityInstance
+ */
+ public static void setMainActivityInstance(MainActivity mainActivityInstance) {
+ MainApplication.mainActivityInstance = mainActivityInstance;
+ }
+
+ public static MainActivity getMainActivityInstance() {
+ return MainApplication.mainActivityInstance;
+ }
+
+
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/Mindmap.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/Mindmap.java
new file mode 100644
index 0000000..4a76957
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/Mindmap.java
@@ -0,0 +1,104 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.acra.ACRA;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+import com.google.analytics.tracking.android.EasyTracker;
+
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Mindmap handles the loading and storing of a mind map document.
+ */
+public class Mindmap {
+
+ /**
+ * the XML DOM document, the mind map
+ */
+ public Document document;
+
+ /**
+ * The currently loaded Uri
+ */
+ private Uri uri;
+
+ /**
+ * The root node of the document.
+ */
+ private MindmapNode rootNode;
+
+ /**
+ * Returns the Uri which is currently loaded in document.
+ * @return Uri
+ */
+ public Uri getUri() {
+ return this.uri;
+ }
+
+ /**
+ * Set the Uri after loading a new document.
+ * @param uri
+ */
+ public void setUri(Uri uri) {
+ this.uri = uri;
+ }
+
+ /**
+ * Loads a mind map (*.mm) XML document into its internal DOM tree
+ * @param inputStream the inputStream to load
+ */
+ public void loadDocument(InputStream inputStream) {
+
+ // start measuring the document load time
+ long loadDocumentStartTime = System.currentTimeMillis();
+
+ // XML document builder
+ DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder docBuilder;
+
+ // load the Mindmap from the InputStream
+ try {
+ docBuilder = docBuilderFactory.newDocumentBuilder();
+ document = docBuilder.parse(inputStream);
+ } catch (ParserConfigurationException e) {
+ ACRA.getErrorReporter().putCustomData("Exception", "ParserConfigurationException");
+ e.printStackTrace();
+ return;
+ } catch (SAXException e) {
+ ACRA.getErrorReporter().putCustomData("Exception", "SAXException");
+ e.printStackTrace();
+ return;
+ } catch (IOException e) {
+ ACRA.getErrorReporter().putCustomData("Exception", "IOException");
+ e.printStackTrace();
+ return;
+ }
+
+ long loadDocumentEndTime = System.currentTimeMillis();
+ EasyTracker.getTracker().sendTiming("document", loadDocumentEndTime-loadDocumentStartTime, "loadDocument", "loadTime");
+ Log.d(MainApplication.TAG, "Document loaded");
+
+ long numNodes = document.getElementsByTagName("node").getLength();
+ EasyTracker.getTracker().sendEvent("document", "loadDocument", "numNodes", numNodes);
+
+ rootNode = new MindmapNode(MainApplication.getStaticApplicationContext(), document.getDocumentElement().getElementsByTagName("node").item(0));
+ }
+
+ /**
+ * Returns the root node of the currently loaded mind map
+ * @return the root node
+ */
+ public MindmapNode getRootNode() {
+ return rootNode;
+ }
+
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNode.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNode.java
new file mode 100644
index 0000000..3d31ada
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/MindmapNode.java
@@ -0,0 +1,363 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.util.Log;
+
+
+
+/**
+ * A MindMapNode is a special type of DOM Node. A DOM Node can be converted to a
+ * MindMapNode if it has type ELEMENT, and tag "node".
+ */
+public class MindmapNode extends LinearLayout {
+
+ /**
+ * the Text of the node (TEXT attribute).
+ */
+ public String text;
+
+ /**
+ * the name of the icon
+ */
+ public String icon_name;
+
+ /**
+ * the Android resource ID of the icon
+ */
+ public int icon_res_id;
+
+ /**
+ * whether the node is expandable, i.e. whether it has child nodes
+ */
+ public boolean isExpandable;
+
+ /**
+ * if the node has a LINK attribute, it will be stored in Uri link
+ */
+ public Uri link;
+
+ /**
+ * the XML DOM node from which this MindMapNode is derived
+ */
+ private Node node;
+
+ /**
+ * whether the node is selected or not, will be set after it was clicked by the user
+ */
+ public boolean selected;
+
+ /**
+ * The list of child MindmapNodes. We support lazy loading.
+ */
+ ArrayList childMindmapNodes;
+
+ /**
+ * True, when this MindmapNode has an inflated layout. Otherwise false.
+ */
+ private Boolean isLayoutInflated = false;
+
+ /**
+ * This constructor is only used to make graphical GUI layout tools happy. If used in running code, it will always throw a IllegalArgumentException.
+ * @deprecated
+ * @param context
+ */
+ public MindmapNode(Context context) {
+ super(context);
+ if ( !isInEditMode() ) {
+ throw new IllegalArgumentException(
+ "The constructor public MindmapNode(Context context) may only be called by graphical layout tools, i.e. when View#isInEditMode() is true. In production, use the constructor public MindmapNode(Context context, Node node).");
+ }
+ }
+
+ /**
+ * Creates a new MindMapNode from Node. The node needs to be of type ELEMENT and have tag "node".
+ * Throws a {@link ClassCastException} if the Node can not be converted to a MindmapNode.
+ * @param node
+ */
+ public MindmapNode(Context context, Node node) {
+
+ super(context);
+
+ // convert the XML Node to a XML Element
+ Element tmp_element;
+ if ( isMindmapNode(node) ) {
+ tmp_element = (Element)node;
+ } else {
+ throw new ClassCastException("Can not convert Node to MindmapNode");
+ }
+
+ // store the Node
+ this.node = node;
+
+ // extract the string (TEXT attribute) of the nodes
+ text = tmp_element.getAttribute("TEXT");
+
+ String linkAttribute = tmp_element.getAttribute("LINK");
+ if ( !linkAttribute.equals("") ) {
+ link = Uri.parse(linkAttribute);
+ }
+
+ // extract icons
+ ArrayList icons = getIcons();
+ String icon="";
+ icon_res_id = 0;
+ if ( icons.size() > 0 ) {
+ icon = icons.get(0);
+ icon_res_id = MainApplication.getStaticApplicationContext().getResources().getIdentifier("@drawable/" + icon, "id", MainApplication.getStaticApplicationContext().getPackageName());
+ }
+
+ // find out if it has sub nodes
+ isExpandable = ( getNumChildMindmapNodes() > 0 );
+
+ // load the layout from the XML file
+ // TODO: we should not inflate the layout straight away, but only when the MindmapNode is displaye (lazy loading)
+// MindmapNode.inflate(context, R.layout.mindmap_node_list_item, this);
+// refreshView();
+
+ }
+
+ /**
+ * Inflates the layout from the XML file if it is not yet inflated
+ * @param context
+ */
+ private void inflateLayout(Context context) {
+ synchronized (isLayoutInflated ) {
+ if (!isLayoutInflated) {
+ MindmapNode.inflate(context, R.layout.mindmap_node_list_item, this);
+ }
+ }
+ }
+
+ @SuppressLint("InlinedApi")
+ public void refreshView() {
+
+ // inflate the layout if we haven't done so yet
+ inflateLayout(getContext());
+
+ // the mindmap_node_list_item consists of a ImageView (icon), a TextView (node text), and another TextView ("+" button)
+ ImageView iconView = (ImageView)findViewById(R.id.icon);
+ iconView.setImageResource(icon_res_id);
+ iconView.setContentDescription(icon_name);
+
+ TextView textView = (TextView) findViewById(R.id.label);
+ textView.setTextColor(getContext().getResources().getColor(android.R.color.primary_text_light));
+ textView.setText(text);
+
+ ImageView expandable = (ImageView) findViewById(R.id.expandable);
+ if ( isExpandable ) {
+ if ( getIsSelected() ) {
+ expandable.setImageResource(R.drawable.minus_alt);
+ } else {
+ expandable.setImageResource(R.drawable.plus_alt);
+ }
+ }
+
+ // if the node is selected and has child nodes, give it a special background
+ if ( getIsSelected() && getNumChildMindmapNodes() > 0 ) {
+ int backgroundColor;
+
+ // menu bar: if we are at least at API 11, the Home button is kind of a back button in the app
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ backgroundColor = getContext().getResources().getColor(android.R.color.holo_blue_bright);
+ } else {
+ backgroundColor = getContext().getResources().getColor(android.R.color.darker_gray);
+ }
+
+ setBackgroundColor(backgroundColor);
+ } else {
+ setBackgroundColor(0);
+ }
+
+ // set the layout parameter
+ // TODO: this should not be necessary. The problem is that the inflate
+ // (in the constructor) loads the XML as child of this LinearView, so
+ // the MindmapNode-LinearView wraps the root LinearView from the
+ // mindmap_node_list_item XML file.
+ setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT));
+ setGravity(Gravity.LEFT | Gravity.CENTER);
+ }
+
+ /**
+ * Selects or deselects this node
+ * @param selected
+ */
+ public void setSelected(boolean selected) {
+ this.selected = selected;
+ }
+
+ /**
+ * Returns whether this node is selected
+ */
+ public boolean getIsSelected() {
+ return this.selected;
+ }
+
+ /**
+ * Checks whether a given node can be converted to a Mindmap node, i.e.
+ * whether it has type ELEMENT_NODE and tag "node"
+ * @param node
+ * @return
+ */
+ public static boolean isMindmapNode(Node node) {
+
+ if (node.getNodeType() == Node.ELEMENT_NODE) {
+ Element element = (Element) node;
+
+ if (element.getTagName().equals("node")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Extracts the list of icons from a node and returns the names of the icons
+ * as ArrayList.
+ *
+ * @return list of names of the icons
+ */
+ private ArrayList getIcons() {
+
+ ArrayList icons = new ArrayList();
+
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+
+ Node n = childNodes.item(i);
+ if ( n.getNodeType() == Node.ELEMENT_NODE ) {
+ Element e = (Element)n;
+
+ if ( e.getTagName().equals("icon") && e.hasAttribute("BUILTIN") ) {
+ icons.add(getDrawableNameFromMindmapIcon(e.getAttribute("BUILTIN")));
+ }
+ }
+ }
+
+ return icons;
+ }
+
+ /**
+ * Mindmap icons have names such as 'button-ok', but resources have to have
+ * names with pattern [a-z0-9_.]. This method translates the Mindmap icon
+ * names to Android resource names.
+ *
+ * @param iconName the icon name as it is specified in the XML
+ * @return the name of the corresponding android resource icon
+ */
+ private String getDrawableNameFromMindmapIcon(String iconName) {
+ Locale locale = MainApplication.getStaticApplicationContext().getResources().getConfiguration().locale;
+ String name = "icon_" + iconName.toLowerCase(locale).replaceAll("[^a-z0-9_.]", "_");
+ name.replaceAll("_$", "");
+ return name;
+ }
+
+ /**
+ * returns the number of child Mindmap nodes
+ * @return
+ */
+ public int getNumChildMindmapNodes() {
+
+ int numMindmapNodes = 0;
+
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+
+ Node n = childNodes.item(i);
+ if ( isMindmapNode(n) ) {
+ numMindmapNodes++;
+ }
+ }
+
+ return numMindmapNodes;
+ }
+
+ /**
+ * The NodeColumn forwards the CreateContextMenu event to the appropriate
+ * MindmapNode, which can then generate the context menu as it likes. Note
+ * that the MindmapNode itself is not registered as the listener for such
+ * events per se, because the NodeColumn first has to decide for which
+ * MindmapNode the event applies.
+ *
+ * @param menu
+ * @param v
+ * @param menuInfo
+ */
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+
+ // build the menu
+ menu.setHeaderTitle(text);
+ menu.setHeaderIcon(icon_res_id);
+
+ // TODO: add a submenu showing all icons
+ // Context menus do not support icons.
+// SubMenu iconMenu = menu.addSubMenu("All icons");
+// ArrayList icons = getIcons();
+// for (int i = 0; i < icons.size(); i++) {
+// String icon = icons.get(i);
+// MenuItem iconMenuItem = iconMenu.add(icon);
+// int iconMenuItemResId = MainApplication.getStaticApplicationContext().getResources().getIdentifier("@drawable/" + icon, "id", MainApplication.getStaticApplicationContext().getPackageName());
+// iconMenuItem.setIcon(MainApplication.getStaticApplicationContext().getResources().getDrawable(iconMenuItemResId));
+// }
+
+ // allow copying the node text
+ menu.add(0, R.id.contextcopy, 0, R.string.copynodetext);
+
+ // add menu to open link, if the node has a hyperlink
+ if ( link != null ) {
+ menu.add(0, R.id.contextopenlink, 0, R.string.openlink);
+ }
+ }
+
+
+ /*
+ * Generates and returns the child nodes of this MindmapNode.
+ * getChildNodes() does lazy loading, i.e. it generates the child nodes on
+ * demand and stores them in childMindmapNodes.
+ * @return ArrayList of this MindmapNode's child nodes
+ */
+ public ArrayList getChildNodes() {
+
+ // if we haven't loaded the childMindmapNodes before
+ if ( childMindmapNodes == null ) {
+
+ // fetch all child DOM Nodes, convert them to MindmapNodes
+ childMindmapNodes = new ArrayList();
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node tmpNode = childNodes.item(i);
+
+ if ( isMindmapNode(tmpNode) ) {
+ MindmapNode mindmapNode = new MindmapNode(getContext(), tmpNode);
+ childMindmapNodes.add(mindmapNode);
+ }
+ }
+ Log.d(MainApplication.TAG, "Returning newly generated childMindmapNodes");
+ return childMindmapNodes;
+ }
+
+ // we already did that before, so return the previous result
+ else {
+ Log.d(MainApplication.TAG, "Returning cached childMindmapNodes");
+ return childMindmapNodes;
+ }
+ }
+}
diff --git a/app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java b/app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java
new file mode 100644
index 0000000..4e77f1c
--- /dev/null
+++ b/app/src/main/java/ch/benediktkoeppel/code/droidplane/NodeColumn.java
@@ -0,0 +1,339 @@
+package ch.benediktkoeppel.code.droidplane;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+//import org.w3c.dom.Node;
+//import org.w3c.dom.NodeList;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Build;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.View;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+
+/**
+ * A column of MindmapNodes, i.e. one level in the mind map. It extends
+ * LinearLayout, and then embeds a ListView. This is because we want to have a
+ * fine border around the ListView and we can only achieve this by having it
+ * wrapped in a LinearLayout with a padding.
+ */
+public class NodeColumn extends LinearLayout implements OnCreateContextMenuListener {
+
+ /**
+ * This translates ListViews to NodeColumns. We need this because the
+ * OnItemClicked Events come with a ListView (i.e. the ListView which was
+ * clicked) as parent, but we need to find out which NodeColumn was clicked.
+ * This would have been a simple cast if NodeColumn extended ListView, but
+ * we extend LinearLayout and wrap the ListView.
+ */
+ private static HashMap listViewToNodeColumn = new HashMap();
+
+ /**
+ * the parent node (i.e. the node that is parent to everything we display in this column)
+ */
+ private MindmapNode parent;
+
+ /**
+ * the list of all MindmapNodes which we display in this column
+ */
+ private ArrayList mindmapNodes;
+
+ /**
+ * the adapter for this column
+ */
+ MindmapNodeAdapter adapter;
+
+ /**
+ * the actual ListView that we'll display
+ */
+ ListView listView;
+
+ /**
+ * This constructor is only used to make graphical GUI layout tools happy. If used in running code, it will always throw a IllegalArgumentException.
+ * @deprecated
+ * @param context
+ */
+ public NodeColumn(Context context) {
+ super(context);
+ if ( !isInEditMode() ) {
+ throw new IllegalArgumentException(
+ "The constructor public NodeColumn(Context context) may only be called by graphical layout tools, i.e. when View#isInEditMode() is true. In production, use the constructor public NodeColumn(Context context, Node parent).");
+ }
+ }
+
+ /**
+ * Creates a new NodeColumn for a parent node. This NodeColumn is a
+ * LinearLayout, which contains a ListView, which displays all child nodes
+ * of the parent node.
+ *
+ * @param context
+ * @param parent
+ */
+ public NodeColumn(Context context, MindmapNode parent) {
+ super(context);
+
+ // extract all elements from the parent node, and add them to the mindmapNodes list
+ mindmapNodes = parent.getChildNodes();
+
+ // store the parent node
+ this.parent = parent;
+
+
+ // define the layout of this LinearView
+ int linearViewHeight = LayoutParams.MATCH_PARENT;
+ int linearViewWidth = getOptimalColumnWidth();
+ LinearLayout.LayoutParams linearViewLayout = new LinearLayout.LayoutParams(linearViewWidth, linearViewHeight);
+ setLayoutParams(linearViewLayout);
+ setPadding(0, 0, 1, 0);
+ setBackgroundColor(context.getResources().getColor(android.R.color.darker_gray));
+
+
+
+ // create a ListView
+ listView = new ListView(context);
+
+ // define the layout of the listView
+ // should be as high as the parent (i.e. full screen height)
+ int listViewHeight = LayoutParams.MATCH_PARENT;
+ int listViewWidth = LayoutParams.MATCH_PARENT;
+ ViewGroup.LayoutParams listViewLayout = new ViewGroup.LayoutParams(listViewWidth, listViewHeight);
+ listView.setLayoutParams(listViewLayout);
+ listView.setBackgroundColor(context.getResources().getColor(android.R.color.background_light));
+
+
+ // create adapter (i.e. data provider) for the column
+ adapter = new MindmapNodeAdapter(getContext(), R.layout.mindmap_node_list_item, mindmapNodes);
+
+ // add the content adapter
+ listView.setAdapter(adapter);
+
+ // store the ListView to NodeColumn mapping in listViewToNodeColumn
+ listViewToNodeColumn.put(listView, this);
+
+ // call NodeColumn's onCreateContextMenu when a context menu for one of
+ // the listView items should be generated.
+ listView.setOnCreateContextMenuListener(this);
+
+ // add the listView to the linearView
+ this.addView(listView);
+ }
+
+ /**
+ * Finds the NodeColumn which contains the given listView.
+ * @param listView
+ * @return the node column which contains listView
+ */
+ public static NodeColumn getNodeColumnFromListView(ListView listView) {
+ return listViewToNodeColumn.get(listView);
+ }
+
+ /**
+ * GUI Helper to detach this LinearView from its parent
+ */
+ public void removeFromParent() {
+ if ( this.getParent() != null ) {
+ ((ViewGroup)this.getParent()).removeView(this);
+ }
+ }
+
+ /**
+ * Sets the width of this column to columnWidth
+ * @param columnWidth width of the column
+ */
+ private void setWidth(int columnWidth) {
+ Log.d(MainApplication.TAG, "Setting column width to " + columnWidth);
+
+ ViewGroup.LayoutParams listViewParam = this.getLayoutParams();
+ listViewParam.width = columnWidth;
+ this.setLayoutParams(listViewParam);
+ }
+
+ /**
+ * Deselects all nodes of this column
+ */
+ public void deselectAllNodes() {
+ // deselect all nodes
+ for (MindmapNode mindmapNode : mindmapNodes) {
+ mindmapNode.setSelected(false);
+ }
+
+ // then notify about the GUI change
+ adapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Returns the parent node of this column.
+ * @return the parent node of this colunn
+ */
+ public MindmapNode getParentNode() {
+ return parent;
+ }
+
+ /**
+ * Calculates the column width which this column should have
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ @SuppressLint("NewApi")
+ private static int getOptimalColumnWidth() {
+
+ // and R.integer.horizontally_visible_panes defines how many columns should be visible side by side
+ // so we need 1/(horizontall_visible_panes) * displayWidth as column width
+ int horizontallyVisiblePanes = MainApplication.getStaticApplicationContext().getResources().getInteger(R.integer.horizontally_visible_panes);
+ android.view.Display display = ((android.view.WindowManager)MainApplication.getStaticApplicationContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
+ int displayWidth;
+
+ // get the Display width. Before HONEYCOMB_MR2, this was display.getWidth, now it is display.getSize
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+ displayWidth = displaySize.x;
+ } else {
+ displayWidth = (int)display.getWidth();
+ }
+ int columnWidth = displayWidth / horizontallyVisiblePanes;
+
+ Log.d(MainApplication.TAG, "Calculated column width = " + columnWidth);
+
+ return columnWidth;
+ }
+
+ /**
+ * Resizes the column to its optimal column width
+ */
+ public void resizeColumnWidth() {
+ setWidth(getOptimalColumnWidth());
+ }
+
+ /**
+ * Fetches the MindmapNode at the given position
+ * @param position the position from which the MindmapNode should be returned
+ * @return MindmapNode
+ */
+ public MindmapNode getNodeAtPosition(int position) {
+ return mindmapNodes.get(position);
+ }
+
+ /**
+ * Sets the color on the node at the specified position
+ * @param position
+ */
+ public void setItemColor(int position) {
+
+ // deselect all nodes
+ for (int i = 0; i < mindmapNodes.size(); i++) {
+ mindmapNodes.get(i).setSelected(false);
+ }
+
+ // then select node at position
+ mindmapNodes.get(position).setSelected(true);
+
+ // then notify about the GUI change
+ adapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Clears the item color on all nodes
+ */
+ public void clearAllItemColor() {
+ for (int i = 0; i < mindmapNodes.size(); i++) {
+ mindmapNodes.get(i).setSelected(false);
+ }
+
+ // then notify about the GUI change
+ adapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Simply a wrapper for ListView's setOnItemClickListener. Technically, the
+ * NodeColumn (which is a LinearView) does not generate OnItemClick Events,
+ * but it's child view (the ListView) does. But it's simpler if the outside
+ * world does not have to care about that detail, so we implement
+ * setOnItemClickListener and just forward the listener to the actual
+ * ListView.
+ * @param listener
+ */
+ public void setOnItemClickListener(OnItemClickListener listener) {
+ listView.setOnItemClickListener(listener);
+ }
+
+
+ /* (non-Javadoc)
+ * This is called when a context menu for one of the list items is generated.
+ * @see android.view.View.OnCreateContextMenuListener#onCreateContextMenu(android.view.ContextMenu, android.view.View, android.view.ContextMenu.ContextMenuInfo)
+ */
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+
+ // get the menu information
+ AdapterView.AdapterContextMenuInfo contextMenuInfo = (AdapterView.AdapterContextMenuInfo)menuInfo;
+
+ // get the clicked node
+ MindmapNode clickedNode = mindmapNodes.get(contextMenuInfo.position);
+
+ // forward the event to the clicked node
+ clickedNode.onCreateContextMenu(menu, v, menuInfo);
+
+ }
+}
+
+
+
+/**
+ * The MindmapNodeAdapter is the data provider for the NodeColumn (respectively
+ * its ListView).
+ */
+class MindmapNodeAdapter extends ArrayAdapter {
+
+ private ArrayList mindmapNodes;
+
+ public MindmapNodeAdapter(Context context, int textViewResourceId, ArrayList mindmapNodes) {
+ super(context, textViewResourceId, mindmapNodes);
+ this.mindmapNodes = mindmapNodes;
+ }
+
+ /* (non-Javadoc)
+ * getView is responsible to return a view for each individual element in the ListView
+ * @param int position: the position in the mindmapNodes array, for which we need to generate a view
+ * @param View convertView: the view we should recycle
+ * @param ViewGroup parent: not sure, is this the NodeColumn for which the Adapter is generating views?
+ * @see android.widget.ArrayAdapter#getView(int, android.view.View, android.view.ViewGroup)
+ */
+ @SuppressLint("InlinedApi")
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+// if ( convertView != null ) {
+// if ( convertView instanceof MindmapNode ) {
+// ((MindmapNode)convertView).discardView();
+// }
+// }
+
+ // when convertView != null, we should take the convertView and update
+ // it appropriately. Android is optimizing the performance
+ // and thus recycling GUI elements. However, we don't want to recycle
+ // anything, because these are genuine Mindmap nodes. Recycling
+ // the view here would show one node twice in the tree, while leaving
+ // out the actual node we should display.
+
+ MindmapNode view = mindmapNodes.get(position);
+
+ // tell the node to refresh it's view
+ view.refreshView();
+
+ return view;
+ }
+
+
+}
diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..124720e
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..3c95942
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..e23f325
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable-xhdpi/minus_alt.png b/app/src/main/res/drawable-xhdpi/minus_alt.png
new file mode 100644
index 0000000..a84aea9
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/minus_alt.png differ
diff --git a/app/src/main/res/drawable-xhdpi/plus_alt.png b/app/src/main/res/drawable-xhdpi/plus_alt.png
new file mode 100644
index 0000000..2646ffd
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/plus_alt.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d2c3f34
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/drawable/icon_0.png b/app/src/main/res/drawable/icon_0.png
new file mode 100644
index 0000000..797c04d
Binary files /dev/null and b/app/src/main/res/drawable/icon_0.png differ
diff --git a/app/src/main/res/drawable/icon_100.png b/app/src/main/res/drawable/icon_100.png
new file mode 100644
index 0000000..20c2a13
Binary files /dev/null and b/app/src/main/res/drawable/icon_100.png differ
diff --git a/app/src/main/res/drawable/icon_25.png b/app/src/main/res/drawable/icon_25.png
new file mode 100644
index 0000000..9e81757
Binary files /dev/null and b/app/src/main/res/drawable/icon_25.png differ
diff --git a/app/src/main/res/drawable/icon_50.png b/app/src/main/res/drawable/icon_50.png
new file mode 100644
index 0000000..9dd37d5
Binary files /dev/null and b/app/src/main/res/drawable/icon_50.png differ
diff --git a/app/src/main/res/drawable/icon_75.png b/app/src/main/res/drawable/icon_75.png
new file mode 100644
index 0000000..ee6e6f3
Binary files /dev/null and b/app/src/main/res/drawable/icon_75.png differ
diff --git a/app/src/main/res/drawable/icon_addition.png b/app/src/main/res/drawable/icon_addition.png
new file mode 100644
index 0000000..931bd85
Binary files /dev/null and b/app/src/main/res/drawable/icon_addition.png differ
diff --git a/app/src/main/res/drawable/icon_attach.png b/app/src/main/res/drawable/icon_attach.png
new file mode 100644
index 0000000..2e4657e
Binary files /dev/null and b/app/src/main/res/drawable/icon_attach.png differ
diff --git a/app/src/main/res/drawable/icon_audio.png b/app/src/main/res/drawable/icon_audio.png
new file mode 100644
index 0000000..d83124b
Binary files /dev/null and b/app/src/main/res/drawable/icon_audio.png differ
diff --git a/app/src/main/res/drawable/icon_back.png b/app/src/main/res/drawable/icon_back.png
new file mode 100644
index 0000000..27930f9
Binary files /dev/null and b/app/src/main/res/drawable/icon_back.png differ
diff --git a/app/src/main/res/drawable/icon_bee.png b/app/src/main/res/drawable/icon_bee.png
new file mode 100644
index 0000000..a5ef8d7
Binary files /dev/null and b/app/src/main/res/drawable/icon_bee.png differ
diff --git a/app/src/main/res/drawable/icon_bell.png b/app/src/main/res/drawable/icon_bell.png
new file mode 100644
index 0000000..354f6d5
Binary files /dev/null and b/app/src/main/res/drawable/icon_bell.png differ
diff --git a/app/src/main/res/drawable/icon_bookmark.png b/app/src/main/res/drawable/icon_bookmark.png
new file mode 100644
index 0000000..2b61e4a
Binary files /dev/null and b/app/src/main/res/drawable/icon_bookmark.png differ
diff --git a/app/src/main/res/drawable/icon_broken_line.png b/app/src/main/res/drawable/icon_broken_line.png
new file mode 100644
index 0000000..cc55444
Binary files /dev/null and b/app/src/main/res/drawable/icon_broken_line.png differ
diff --git a/app/src/main/res/drawable/icon_button.png b/app/src/main/res/drawable/icon_button.png
new file mode 100644
index 0000000..70e4314
Binary files /dev/null and b/app/src/main/res/drawable/icon_button.png differ
diff --git a/app/src/main/res/drawable/icon_button_cancel.png b/app/src/main/res/drawable/icon_button_cancel.png
new file mode 100644
index 0000000..0711270
Binary files /dev/null and b/app/src/main/res/drawable/icon_button_cancel.png differ
diff --git a/app/src/main/res/drawable/icon_button_ok.png b/app/src/main/res/drawable/icon_button_ok.png
new file mode 100644
index 0000000..dd1c408
Binary files /dev/null and b/app/src/main/res/drawable/icon_button_ok.png differ
diff --git a/app/src/main/res/drawable/icon_calendar.png b/app/src/main/res/drawable/icon_calendar.png
new file mode 100644
index 0000000..c7fa6be
Binary files /dev/null and b/app/src/main/res/drawable/icon_calendar.png differ
diff --git a/app/src/main/res/drawable/icon_checked.png b/app/src/main/res/drawable/icon_checked.png
new file mode 100644
index 0000000..a75a4b5
Binary files /dev/null and b/app/src/main/res/drawable/icon_checked.png differ
diff --git a/app/src/main/res/drawable/icon_clanbomber.png b/app/src/main/res/drawable/icon_clanbomber.png
new file mode 100644
index 0000000..270f771
Binary files /dev/null and b/app/src/main/res/drawable/icon_clanbomber.png differ
diff --git a/app/src/main/res/drawable/icon_clock.png b/app/src/main/res/drawable/icon_clock.png
new file mode 100644
index 0000000..380c419
Binary files /dev/null and b/app/src/main/res/drawable/icon_clock.png differ
diff --git a/app/src/main/res/drawable/icon_clock2.png b/app/src/main/res/drawable/icon_clock2.png
new file mode 100644
index 0000000..2bfcf44
Binary files /dev/null and b/app/src/main/res/drawable/icon_clock2.png differ
diff --git a/app/src/main/res/drawable/icon_closed.png b/app/src/main/res/drawable/icon_closed.png
new file mode 100644
index 0000000..7f0fa48
Binary files /dev/null and b/app/src/main/res/drawable/icon_closed.png differ
diff --git a/app/src/main/res/drawable/icon_decrypted.png b/app/src/main/res/drawable/icon_decrypted.png
new file mode 100644
index 0000000..90b0193
Binary files /dev/null and b/app/src/main/res/drawable/icon_decrypted.png differ
diff --git a/app/src/main/res/drawable/icon_desktop_new.png b/app/src/main/res/drawable/icon_desktop_new.png
new file mode 100644
index 0000000..0337ddd
Binary files /dev/null and b/app/src/main/res/drawable/icon_desktop_new.png differ
diff --git a/app/src/main/res/drawable/icon_division.png b/app/src/main/res/drawable/icon_division.png
new file mode 100644
index 0000000..dbb04e4
Binary files /dev/null and b/app/src/main/res/drawable/icon_division.png differ
diff --git a/app/src/main/res/drawable/icon_down.png b/app/src/main/res/drawable/icon_down.png
new file mode 100644
index 0000000..9ea6d45
Binary files /dev/null and b/app/src/main/res/drawable/icon_down.png differ
diff --git a/app/src/main/res/drawable/icon_edit.png b/app/src/main/res/drawable/icon_edit.png
new file mode 100644
index 0000000..aeca7aa
Binary files /dev/null and b/app/src/main/res/drawable/icon_edit.png differ
diff --git a/app/src/main/res/drawable/icon_encrypted.png b/app/src/main/res/drawable/icon_encrypted.png
new file mode 100644
index 0000000..4f15c63
Binary files /dev/null and b/app/src/main/res/drawable/icon_encrypted.png differ
diff --git a/app/src/main/res/drawable/icon_executable.png b/app/src/main/res/drawable/icon_executable.png
new file mode 100644
index 0000000..d4d9f96
Binary files /dev/null and b/app/src/main/res/drawable/icon_executable.png differ
diff --git a/app/src/main/res/drawable/icon_family.png b/app/src/main/res/drawable/icon_family.png
new file mode 100644
index 0000000..abeb561
Binary files /dev/null and b/app/src/main/res/drawable/icon_family.png differ
diff --git a/app/src/main/res/drawable/icon_fema.png b/app/src/main/res/drawable/icon_fema.png
new file mode 100644
index 0000000..f2691a9
Binary files /dev/null and b/app/src/main/res/drawable/icon_fema.png differ
diff --git a/app/src/main/res/drawable/icon_female1.png b/app/src/main/res/drawable/icon_female1.png
new file mode 100644
index 0000000..a67091f
Binary files /dev/null and b/app/src/main/res/drawable/icon_female1.png differ
diff --git a/app/src/main/res/drawable/icon_female2.png b/app/src/main/res/drawable/icon_female2.png
new file mode 100644
index 0000000..7202015
Binary files /dev/null and b/app/src/main/res/drawable/icon_female2.png differ
diff --git a/app/src/main/res/drawable/icon_females.png b/app/src/main/res/drawable/icon_females.png
new file mode 100644
index 0000000..84b1349
Binary files /dev/null and b/app/src/main/res/drawable/icon_females.png differ
diff --git a/app/src/main/res/drawable/icon_flag.png b/app/src/main/res/drawable/icon_flag.png
new file mode 100644
index 0000000..be26f26
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag.png differ
diff --git a/app/src/main/res/drawable/icon_flag_black.png b/app/src/main/res/drawable/icon_flag_black.png
new file mode 100644
index 0000000..3394ebb
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_black.png differ
diff --git a/app/src/main/res/drawable/icon_flag_blue.png b/app/src/main/res/drawable/icon_flag_blue.png
new file mode 100644
index 0000000..3bee08a
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_blue.png differ
diff --git a/app/src/main/res/drawable/icon_flag_green.png b/app/src/main/res/drawable/icon_flag_green.png
new file mode 100644
index 0000000..07850a2
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_green.png differ
diff --git a/app/src/main/res/drawable/icon_flag_orange.png b/app/src/main/res/drawable/icon_flag_orange.png
new file mode 100644
index 0000000..a5a4346
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_orange.png differ
diff --git a/app/src/main/res/drawable/icon_flag_pink.png b/app/src/main/res/drawable/icon_flag_pink.png
new file mode 100644
index 0000000..221114c
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_pink.png differ
diff --git a/app/src/main/res/drawable/icon_flag_yellow.png b/app/src/main/res/drawable/icon_flag_yellow.png
new file mode 100644
index 0000000..c29b9cf
Binary files /dev/null and b/app/src/main/res/drawable/icon_flag_yellow.png differ
diff --git a/app/src/main/res/drawable/icon_folder.png b/app/src/main/res/drawable/icon_folder.png
new file mode 100644
index 0000000..f34809f
Binary files /dev/null and b/app/src/main/res/drawable/icon_folder.png differ
diff --git a/app/src/main/res/drawable/icon_forward.png b/app/src/main/res/drawable/icon_forward.png
new file mode 100644
index 0000000..af3668f
Binary files /dev/null and b/app/src/main/res/drawable/icon_forward.png differ
diff --git a/app/src/main/res/drawable/icon_freemind_butterfly.png b/app/src/main/res/drawable/icon_freemind_butterfly.png
new file mode 100644
index 0000000..896188f
Binary files /dev/null and b/app/src/main/res/drawable/icon_freemind_butterfly.png differ
diff --git a/app/src/main/res/drawable/icon_full_0.png b/app/src/main/res/drawable/icon_full_0.png
new file mode 100644
index 0000000..253af36
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_0.png differ
diff --git a/app/src/main/res/drawable/icon_full_1.png b/app/src/main/res/drawable/icon_full_1.png
new file mode 100644
index 0000000..a2adb49
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_1.png differ
diff --git a/app/src/main/res/drawable/icon_full_2.png b/app/src/main/res/drawable/icon_full_2.png
new file mode 100644
index 0000000..4796906
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_2.png differ
diff --git a/app/src/main/res/drawable/icon_full_3.png b/app/src/main/res/drawable/icon_full_3.png
new file mode 100644
index 0000000..66acf1c
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_3.png differ
diff --git a/app/src/main/res/drawable/icon_full_4.png b/app/src/main/res/drawable/icon_full_4.png
new file mode 100644
index 0000000..595d587
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_4.png differ
diff --git a/app/src/main/res/drawable/icon_full_5.png b/app/src/main/res/drawable/icon_full_5.png
new file mode 100644
index 0000000..bffcee9
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_5.png differ
diff --git a/app/src/main/res/drawable/icon_full_6.png b/app/src/main/res/drawable/icon_full_6.png
new file mode 100644
index 0000000..9618296
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_6.png differ
diff --git a/app/src/main/res/drawable/icon_full_7.png b/app/src/main/res/drawable/icon_full_7.png
new file mode 100644
index 0000000..69f057d
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_7.png differ
diff --git a/app/src/main/res/drawable/icon_full_8.png b/app/src/main/res/drawable/icon_full_8.png
new file mode 100644
index 0000000..64b20a8
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_8.png differ
diff --git a/app/src/main/res/drawable/icon_full_9.png b/app/src/main/res/drawable/icon_full_9.png
new file mode 100644
index 0000000..754226f
Binary files /dev/null and b/app/src/main/res/drawable/icon_full_9.png differ
diff --git a/app/src/main/res/drawable/icon_go.png b/app/src/main/res/drawable/icon_go.png
new file mode 100644
index 0000000..0fe8843
Binary files /dev/null and b/app/src/main/res/drawable/icon_go.png differ
diff --git a/app/src/main/res/drawable/icon_gohome.png b/app/src/main/res/drawable/icon_gohome.png
new file mode 100644
index 0000000..a47001d
Binary files /dev/null and b/app/src/main/res/drawable/icon_gohome.png differ
diff --git a/app/src/main/res/drawable/icon_group.png b/app/src/main/res/drawable/icon_group.png
new file mode 100644
index 0000000..c2c76e5
Binary files /dev/null and b/app/src/main/res/drawable/icon_group.png differ
diff --git a/app/src/main/res/drawable/icon_help.png b/app/src/main/res/drawable/icon_help.png
new file mode 100644
index 0000000..31c4e67
Binary files /dev/null and b/app/src/main/res/drawable/icon_help.png differ
diff --git a/app/src/main/res/drawable/icon_hourglass.png b/app/src/main/res/drawable/icon_hourglass.png
new file mode 100644
index 0000000..84f9cdd
Binary files /dev/null and b/app/src/main/res/drawable/icon_hourglass.png differ
diff --git a/app/src/main/res/drawable/icon_idea.png b/app/src/main/res/drawable/icon_idea.png
new file mode 100644
index 0000000..b2c4be8
Binary files /dev/null and b/app/src/main/res/drawable/icon_idea.png differ
diff --git a/app/src/main/res/drawable/icon_image.png b/app/src/main/res/drawable/icon_image.png
new file mode 100644
index 0000000..bc38937
Binary files /dev/null and b/app/src/main/res/drawable/icon_image.png differ
diff --git a/app/src/main/res/drawable/icon_info.png b/app/src/main/res/drawable/icon_info.png
new file mode 100644
index 0000000..c7719ba
Binary files /dev/null and b/app/src/main/res/drawable/icon_info.png differ
diff --git a/app/src/main/res/drawable/icon_internet.png b/app/src/main/res/drawable/icon_internet.png
new file mode 100644
index 0000000..4824f4f
Binary files /dev/null and b/app/src/main/res/drawable/icon_internet.png differ
diff --git a/app/src/main/res/drawable/icon_internet_warning.png b/app/src/main/res/drawable/icon_internet_warning.png
new file mode 100644
index 0000000..c62303f
Binary files /dev/null and b/app/src/main/res/drawable/icon_internet_warning.png differ
diff --git a/app/src/main/res/drawable/icon_kaddressbook.png b/app/src/main/res/drawable/icon_kaddressbook.png
new file mode 100644
index 0000000..e1067af
Binary files /dev/null and b/app/src/main/res/drawable/icon_kaddressbook.png differ
diff --git a/app/src/main/res/drawable/icon_kmail.png b/app/src/main/res/drawable/icon_kmail.png
new file mode 100644
index 0000000..56325b6
Binary files /dev/null and b/app/src/main/res/drawable/icon_kmail.png differ
diff --git a/app/src/main/res/drawable/icon_knotify.png b/app/src/main/res/drawable/icon_knotify.png
new file mode 100644
index 0000000..9bea5a0
Binary files /dev/null and b/app/src/main/res/drawable/icon_knotify.png differ
diff --git a/app/src/main/res/drawable/icon_korn.png b/app/src/main/res/drawable/icon_korn.png
new file mode 100644
index 0000000..275f026
Binary files /dev/null and b/app/src/main/res/drawable/icon_korn.png differ
diff --git a/app/src/main/res/drawable/icon_ksmiletris.png b/app/src/main/res/drawable/icon_ksmiletris.png
new file mode 100644
index 0000000..54ab5b0
Binary files /dev/null and b/app/src/main/res/drawable/icon_ksmiletris.png differ
diff --git a/app/src/main/res/drawable/icon_launch.png b/app/src/main/res/drawable/icon_launch.png
new file mode 100644
index 0000000..30f8e3d
Binary files /dev/null and b/app/src/main/res/drawable/icon_launch.png differ
diff --git a/app/src/main/res/drawable/icon_licq.png b/app/src/main/res/drawable/icon_licq.png
new file mode 100644
index 0000000..5ea5e4d
Binary files /dev/null and b/app/src/main/res/drawable/icon_licq.png differ
diff --git a/app/src/main/res/drawable/icon_list.png b/app/src/main/res/drawable/icon_list.png
new file mode 100644
index 0000000..1827654
Binary files /dev/null and b/app/src/main/res/drawable/icon_list.png differ
diff --git a/app/src/main/res/drawable/icon_mail.png b/app/src/main/res/drawable/icon_mail.png
new file mode 100644
index 0000000..66553c3
Binary files /dev/null and b/app/src/main/res/drawable/icon_mail.png differ
diff --git a/app/src/main/res/drawable/icon_male1.png b/app/src/main/res/drawable/icon_male1.png
new file mode 100644
index 0000000..6dfa853
Binary files /dev/null and b/app/src/main/res/drawable/icon_male1.png differ
diff --git a/app/src/main/res/drawable/icon_male2.png b/app/src/main/res/drawable/icon_male2.png
new file mode 100644
index 0000000..f2e9819
Binary files /dev/null and b/app/src/main/res/drawable/icon_male2.png differ
diff --git a/app/src/main/res/drawable/icon_males.png b/app/src/main/res/drawable/icon_males.png
new file mode 100644
index 0000000..54b7fe4
Binary files /dev/null and b/app/src/main/res/drawable/icon_males.png differ
diff --git a/app/src/main/res/drawable/icon_messagebox_warning.png b/app/src/main/res/drawable/icon_messagebox_warning.png
new file mode 100644
index 0000000..dfc69db
Binary files /dev/null and b/app/src/main/res/drawable/icon_messagebox_warning.png differ
diff --git a/app/src/main/res/drawable/icon_mindmap.png b/app/src/main/res/drawable/icon_mindmap.png
new file mode 100644
index 0000000..8f70fac
Binary files /dev/null and b/app/src/main/res/drawable/icon_mindmap.png differ
diff --git a/app/src/main/res/drawable/icon_multiplication.png b/app/src/main/res/drawable/icon_multiplication.png
new file mode 100644
index 0000000..dc607a5
Binary files /dev/null and b/app/src/main/res/drawable/icon_multiplication.png differ
diff --git a/app/src/main/res/drawable/icon_narrative.png b/app/src/main/res/drawable/icon_narrative.png
new file mode 100644
index 0000000..209350c
Binary files /dev/null and b/app/src/main/res/drawable/icon_narrative.png differ
diff --git a/app/src/main/res/drawable/icon_negative.png b/app/src/main/res/drawable/icon_negative.png
new file mode 100644
index 0000000..26dbff4
Binary files /dev/null and b/app/src/main/res/drawable/icon_negative.png differ
diff --git a/app/src/main/res/drawable/icon_neutral.png b/app/src/main/res/drawable/icon_neutral.png
new file mode 100644
index 0000000..4f9eb54
Binary files /dev/null and b/app/src/main/res/drawable/icon_neutral.png differ
diff --git a/app/src/main/res/drawable/icon_password.png b/app/src/main/res/drawable/icon_password.png
new file mode 100644
index 0000000..f7e8e59
Binary files /dev/null and b/app/src/main/res/drawable/icon_password.png differ
diff --git a/app/src/main/res/drawable/icon_pencil.png b/app/src/main/res/drawable/icon_pencil.png
new file mode 100644
index 0000000..b371127
Binary files /dev/null and b/app/src/main/res/drawable/icon_pencil.png differ
diff --git a/app/src/main/res/drawable/icon_penguin.png b/app/src/main/res/drawable/icon_penguin.png
new file mode 100644
index 0000000..808dabd
Binary files /dev/null and b/app/src/main/res/drawable/icon_penguin.png differ
diff --git a/app/src/main/res/drawable/icon_positive.png b/app/src/main/res/drawable/icon_positive.png
new file mode 100644
index 0000000..cf254b1
Binary files /dev/null and b/app/src/main/res/drawable/icon_positive.png differ
diff --git a/app/src/main/res/drawable/icon_prepare.png b/app/src/main/res/drawable/icon_prepare.png
new file mode 100644
index 0000000..260be28
Binary files /dev/null and b/app/src/main/res/drawable/icon_prepare.png differ
diff --git a/app/src/main/res/drawable/icon_redo.png b/app/src/main/res/drawable/icon_redo.png
new file mode 100644
index 0000000..69cff22
Binary files /dev/null and b/app/src/main/res/drawable/icon_redo.png differ
diff --git a/app/src/main/res/drawable/icon_revision.png b/app/src/main/res/drawable/icon_revision.png
new file mode 100644
index 0000000..9e54025
Binary files /dev/null and b/app/src/main/res/drawable/icon_revision.png differ
diff --git a/app/src/main/res/drawable/icon_smiley_angry.png b/app/src/main/res/drawable/icon_smiley_angry.png
new file mode 100644
index 0000000..43d0871
Binary files /dev/null and b/app/src/main/res/drawable/icon_smiley_angry.png differ
diff --git a/app/src/main/res/drawable/icon_smiley_neutral.png b/app/src/main/res/drawable/icon_smiley_neutral.png
new file mode 100644
index 0000000..89b9b27
Binary files /dev/null and b/app/src/main/res/drawable/icon_smiley_neutral.png differ
diff --git a/app/src/main/res/drawable/icon_smiley_oh.png b/app/src/main/res/drawable/icon_smiley_oh.png
new file mode 100644
index 0000000..3e1284c
Binary files /dev/null and b/app/src/main/res/drawable/icon_smiley_oh.png differ
diff --git a/app/src/main/res/drawable/icon_smily_bad.png b/app/src/main/res/drawable/icon_smily_bad.png
new file mode 100644
index 0000000..15bd61c
Binary files /dev/null and b/app/src/main/res/drawable/icon_smily_bad.png differ
diff --git a/app/src/main/res/drawable/icon_stop.png b/app/src/main/res/drawable/icon_stop.png
new file mode 100644
index 0000000..a092f72
Binary files /dev/null and b/app/src/main/res/drawable/icon_stop.png differ
diff --git a/app/src/main/res/drawable/icon_stop_sign.png b/app/src/main/res/drawable/icon_stop_sign.png
new file mode 100644
index 0000000..a978ed2
Binary files /dev/null and b/app/src/main/res/drawable/icon_stop_sign.png differ
diff --git a/app/src/main/res/drawable/icon_subtraction.png b/app/src/main/res/drawable/icon_subtraction.png
new file mode 100644
index 0000000..924fb6f
Binary files /dev/null and b/app/src/main/res/drawable/icon_subtraction.png differ
diff --git a/app/src/main/res/drawable/icon_unchecked.png b/app/src/main/res/drawable/icon_unchecked.png
new file mode 100644
index 0000000..38e2b19
Binary files /dev/null and b/app/src/main/res/drawable/icon_unchecked.png differ
diff --git a/app/src/main/res/drawable/icon_up.png b/app/src/main/res/drawable/icon_up.png
new file mode 100644
index 0000000..284181c
Binary files /dev/null and b/app/src/main/res/drawable/icon_up.png differ
diff --git a/app/src/main/res/drawable/icon_user_icon.png b/app/src/main/res/drawable/icon_user_icon.png
new file mode 100644
index 0000000..d43c949
Binary files /dev/null and b/app/src/main/res/drawable/icon_user_icon.png differ
diff --git a/app/src/main/res/drawable/icon_very_negative.png b/app/src/main/res/drawable/icon_very_negative.png
new file mode 100644
index 0000000..9588243
Binary files /dev/null and b/app/src/main/res/drawable/icon_very_negative.png differ
diff --git a/app/src/main/res/drawable/icon_very_positive.png b/app/src/main/res/drawable/icon_very_positive.png
new file mode 100644
index 0000000..e3269ce
Binary files /dev/null and b/app/src/main/res/drawable/icon_very_positive.png differ
diff --git a/app/src/main/res/drawable/icon_video.png b/app/src/main/res/drawable/icon_video.png
new file mode 100644
index 0000000..af904d5
Binary files /dev/null and b/app/src/main/res/drawable/icon_video.png differ
diff --git a/app/src/main/res/drawable/icon_wizard.png b/app/src/main/res/drawable/icon_wizard.png
new file mode 100644
index 0000000..be4f3e8
Binary files /dev/null and b/app/src/main/res/drawable/icon_wizard.png differ
diff --git a/app/src/main/res/drawable/icon_xmag.png b/app/src/main/res/drawable/icon_xmag.png
new file mode 100644
index 0000000..08d31e0
Binary files /dev/null and b/app/src/main/res/drawable/icon_xmag.png differ
diff --git a/app/src/main/res/drawable/icon_yes.png b/app/src/main/res/drawable/icon_yes.png
new file mode 100644
index 0000000..d3c09a2
Binary files /dev/null and b/app/src/main/res/drawable/icon_yes.png differ
diff --git a/app/src/main/res/drawable/minus_alt.png b/app/src/main/res/drawable/minus_alt.png
new file mode 100644
index 0000000..ca7b695
Binary files /dev/null and b/app/src/main/res/drawable/minus_alt.png differ
diff --git a/app/src/main/res/drawable/plus_alt.png b/app/src/main/res/drawable/plus_alt.png
new file mode 100644
index 0000000..cbf339f
Binary files /dev/null and b/app/src/main/res/drawable/plus_alt.png differ
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 0000000..9613049
--- /dev/null
+++ b/app/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..9613049
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/mindmap_node_list_item.xml b/app/src/main/res/layout/mindmap_node_list_item.xml
new file mode 100644
index 0000000..7bb769d
--- /dev/null
+++ b/app/src/main/res/layout/mindmap_node_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 0000000..d4aee4f
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/raw/example.mm b/app/src/main/res/raw/example.mm
new file mode 100644
index 0000000..de5a377
--- /dev/null
+++ b/app/src/main/res/raw/example.mm
@@ -0,0 +1,56 @@
+
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
new file mode 100644
index 0000000..34a51f0
--- /dev/null
+++ b/app/src/main/res/values-land/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+ 16dp
+ 16dp
+
+
+ 3
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml
new file mode 100644
index 0000000..1ba777d
--- /dev/null
+++ b/app/src/main/res/values-sw600dp/dimens.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-sw720dp-land/dimens.xml b/app/src/main/res/values-sw720dp-land/dimens.xml
new file mode 100644
index 0000000..eee741a
--- /dev/null
+++ b/app/src/main/res/values-sw720dp-land/dimens.xml
@@ -0,0 +1,9 @@
+
+
+
+ 128dp
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v11/styles.xml b/app/src/main/res/values-v11/styles.xml
new file mode 100644
index 0000000..541752f
--- /dev/null
+++ b/app/src/main/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v14/styles.xml b/app/src/main/res/values-v14/styles.xml
new file mode 100644
index 0000000..f20e015
--- /dev/null
+++ b/app/src/main/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/analytics.xml b/app/src/main/res/values/analytics.xml
new file mode 100644
index 0000000..6aa6590
--- /dev/null
+++ b/app/src/main/res/values/analytics.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ UA-39295651-1
+
+
+ true
+
+
+ true
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..78d5c30
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+ 16dp
+ 16dp
+
+
+ 1
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/identifiers.xml b/app/src/main/res/values/identifiers.xml
new file mode 100644
index 0000000..27017ea
--- /dev/null
+++ b/app/src/main/res/values/identifiers.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..56554b5
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,16 @@
+
+
+ DroidPlane
+ Settings
+ MainActivity
+ Up
+ Top
+ Yes
+ OK
+ Sorry, I could not find the file you wanted to open!
+ no icon
+ Please start DroidPlane by opening a *.mm file"
+ Copy Node Text
+ Open Link
+ Help
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..4a10ca4
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..e26cdee
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,15 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.0.0'
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
diff --git a/droidplane.iml b/droidplane.iml
new file mode 100644
index 0000000..2a02201
--- /dev/null
+++ b/droidplane.iml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..0c71e76
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Apr 10 15:27:10 PDT 2013
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/test/With Attachment/Attachment.mm b/test/With Attachment/Attachment.mm
new file mode 100644
index 0000000..c7ab9a3
--- /dev/null
+++ b/test/With Attachment/Attachment.mm
@@ -0,0 +1,62 @@
+
diff --git a/test/With Attachment/subfolder/a/whyfp.pdf b/test/With Attachment/subfolder/a/whyfp.pdf
new file mode 100644
index 0000000..69b94cf
Binary files /dev/null and b/test/With Attachment/subfolder/a/whyfp.pdf differ