Browse Source

project upload

Martin Herbers 4 years ago
parent
commit
2905a10dcb
88 changed files with 3247 additions and 0 deletions
  1. 7 0
      .gitignore
  2. 22 0
      aFileChooser/build.gradle
  3. 22 0
      aFileChooser/src/main/AndroidManifest.xml
  4. 233 0
      aFileChooser/src/main/java/com/ianhanniballake/localstorage/LocalStorageProvider.java
  5. 218 0
      aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileChooserActivity.java
  6. 121 0
      aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileListAdapter.java
  7. 136 0
      aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileListFragment.java
  8. 149 0
      aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileLoader.java
  9. 570 0
      aFileChooser/src/main/java/com/ipaulpro/afilechooser/utils/FileUtils.java
  10. BIN
      aFileChooser/src/main/res/drawable-hdpi/ic_chooser.png
  11. BIN
      aFileChooser/src/main/res/drawable-hdpi/ic_file.png
  12. BIN
      aFileChooser/src/main/res/drawable-hdpi/ic_folder.png
  13. BIN
      aFileChooser/src/main/res/drawable-hdpi/ic_provider.png
  14. BIN
      aFileChooser/src/main/res/drawable-mdpi/ic_chooser.png
  15. BIN
      aFileChooser/src/main/res/drawable-mdpi/ic_file.png
  16. BIN
      aFileChooser/src/main/res/drawable-mdpi/ic_folder.png
  17. BIN
      aFileChooser/src/main/res/drawable-mdpi/ic_provider.png
  18. BIN
      aFileChooser/src/main/res/drawable-xhdpi/ic_chooser.png
  19. BIN
      aFileChooser/src/main/res/drawable-xhdpi/ic_file.png
  20. BIN
      aFileChooser/src/main/res/drawable-xhdpi/ic_folder.png
  21. BIN
      aFileChooser/src/main/res/drawable-xhdpi/ic_provider.png
  22. BIN
      aFileChooser/src/main/res/drawable-xxhdpi/ic_chooser.png
  23. BIN
      aFileChooser/src/main/res/drawable-xxhdpi/ic_file.png
  24. BIN
      aFileChooser/src/main/res/drawable-xxhdpi/ic_folder.png
  25. BIN
      aFileChooser/src/main/res/drawable-xxhdpi/ic_provider.png
  26. 20 0
      aFileChooser/src/main/res/layout/file.xml
  27. 8 0
      aFileChooser/src/main/res/values-ca/strings.xml
  28. 7 0
      aFileChooser/src/main/res/values-de/strings.xml
  29. 8 0
      aFileChooser/src/main/res/values-es/strings.xml
  30. 8 0
      aFileChooser/src/main/res/values-fr/strings.xml
  31. 8 0
      aFileChooser/src/main/res/values-ga/strings.xml
  32. 8 0
      aFileChooser/src/main/res/values-it/strings.xml
  33. 8 0
      aFileChooser/src/main/res/values-ko/strings.xml
  34. 8 0
      aFileChooser/src/main/res/values-pl/strings.xml
  35. 8 0
      aFileChooser/src/main/res/values-pt-rBR/strings.xml
  36. 8 0
      aFileChooser/src/main/res/values-ru/strings.xml
  37. 19 0
      aFileChooser/src/main/res/values-v11/strings.xml
  38. 7 0
      aFileChooser/src/main/res/values-v19/bool.xml
  39. 7 0
      aFileChooser/src/main/res/values/bool.xml
  40. 20 0
      aFileChooser/src/main/res/values/dimens.xml
  41. 23 0
      aFileChooser/src/main/res/values/strings.xml
  42. 29 0
      aFileChooser/src/main/res/values/styles.xml
  43. 89 0
      aFileChooser/src/main/res/xml/mimetypes.xml
  44. 1 0
      app/.gitignore
  45. 31 0
      app/build.gradle
  46. 25 0
      app/proguard-rules.pro
  47. 26 0
      app/src/androidTest/java/de/tu_darmstadt/informatik/tk/olir/ExampleInstrumentedTest.java
  48. 44 0
      app/src/main/AndroidManifest.xml
  49. 133 0
      app/src/main/java/de/tu_darmstadt/informatik/tk/olir/DrawView.java
  50. 71 0
      app/src/main/java/de/tu_darmstadt/informatik/tk/olir/MainActivity.java
  51. 125 0
      app/src/main/java/de/tu_darmstadt/informatik/tk/olir/ObjectsActivity.java
  52. 121 0
      app/src/main/java/de/tu_darmstadt/informatik/tk/olir/Parser.java
  53. 44 0
      app/src/main/java/de/tu_darmstadt/informatik/tk/olir/ReadingActivity.java
  54. 45 0
      app/src/main/res/layout/activity_main.xml
  55. 306 0
      app/src/main/res/layout/activity_objects.xml
  56. 51 0
      app/src/main/res/layout/activity_reading.xml
  57. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  58. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  59. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  60. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  61. BIN
      app/src/main/res/mipmap-xxxhdpi/acceleration.png
  62. BIN
      app/src/main/res/mipmap-xxxhdpi/add_circle.png
  63. BIN
      app/src/main/res/mipmap-xxxhdpi/cog.png
  64. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  65. BIN
      app/src/main/res/mipmap-xxxhdpi/plus.png
  66. BIN
      app/src/main/res/mipmap-xxxhdpi/pressure.png
  67. BIN
      app/src/main/res/mipmap-xxxhdpi/rotation.png
  68. BIN
      app/src/main/res/mipmap-xxxhdpi/temperature.png
  69. BIN
      app/src/main/res/mipmap-xxxhdpi/weight.png
  70. 5 0
      app/src/main/res/raw/object_specs_acceleration.txt
  71. 5 0
      app/src/main/res/raw/object_specs_load.txt
  72. 5 0
      app/src/main/res/raw/object_specs_pressure.txt
  73. 5 0
      app/src/main/res/raw/object_specs_temp_falling.txt
  74. 5 0
      app/src/main/res/raw/object_specs_temp_rising.txt
  75. 5 0
      app/src/main/res/raw/object_specs_tilt180.txt
  76. 5 0
      app/src/main/res/raw/object_specs_tilt90.txt
  77. 6 0
      app/src/main/res/values/colors.xml
  78. 3 0
      app/src/main/res/values/dimens.xml
  79. 5 0
      app/src/main/res/values/strings.xml
  80. 20 0
      app/src/main/res/values/styles.xml
  81. 17 0
      app/src/test/java/de/tu_darmstadt/informatik/tk/olir/ExampleUnitTest.java
  82. 24 0
      build.gradle
  83. 17 0
      gradle.properties
  84. 6 0
      gradle/wrapper/gradle-wrapper.properties
  85. 160 0
      gradlew
  86. 90 0
      gradlew.bat
  87. 68 0
      import-summary.txt
  88. 2 0
      settings.gradle

+ 7 - 0
.gitignore

@@ -32,6 +32,13 @@ proguard/
 # Android Studio captures folder
 captures/
 
+# Android Studio
+*.iml
+.idea
+.DS_Store
+.externalNativeBuild
+Thumbs.db
+
 # ---> Java
 *.class
 

+ 22 - 0
aFileChooser/build.gradle

@@ -0,0 +1,22 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 27
+
+    defaultConfig {
+        minSdkVersion 14
+        targetSdkVersion 27
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+}
+
+dependencies {
+    compile 'com.android.support:support-v4:27.+'
+
+}

+ 22 - 0
aFileChooser/src/main/AndroidManifest.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 
+ * Copyright (C) 2011 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.ipaulpro.afilechooser">
+
+    <uses-sdk android:minSdkVersion="7" />
+
+</manifest>

+ 233 - 0
aFileChooser/src/main/java/com/ianhanniballake/localstorage/LocalStorageProvider.java

@@ -0,0 +1,233 @@
+
+package com.ianhanniballake.localstorage;
+
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.os.CancellationSignal;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.ipaulpro.afilechooser.R;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class LocalStorageProvider extends DocumentsProvider {
+
+    public static final String AUTHORITY = "com.ianhanniballake.localstorage.documents";
+
+    /**
+     * Default root projection: everything but Root.COLUMN_MIME_TYPES
+     */
+    private final static String[] DEFAULT_ROOT_PROJECTION = new String[] {
+            Root.COLUMN_ROOT_ID,
+            Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_ICON,
+            Root.COLUMN_AVAILABLE_BYTES
+    };
+    /**
+     * Default document projection: everything but Document.COLUMN_ICON and
+     * Document.COLUMN_SUMMARY
+     */
+    private final static String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID,
+            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS, Document.COLUMN_MIME_TYPE,
+            Document.COLUMN_SIZE,
+            Document.COLUMN_LAST_MODIFIED
+    };
+
+    @Override
+    public Cursor queryRoots(final String[] projection) throws FileNotFoundException {
+        // Create a cursor with either the requested fields, or the default
+        // projection if "projection" is null.
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_ROOT_PROJECTION);
+        // Add Home directory
+        File homeDir = Environment.getExternalStorageDirectory();
+        final MatrixCursor.RowBuilder row = result.newRow();
+        // These columns are required
+        row.add(Root.COLUMN_ROOT_ID, homeDir.getAbsolutePath());
+        row.add(Root.COLUMN_DOCUMENT_ID, homeDir.getAbsolutePath());
+        row.add(Root.COLUMN_TITLE, getContext().getString(R.string.internal_storage));
+        row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE);
+        row.add(Root.COLUMN_ICON, R.drawable.ic_provider);
+        // These columns are optional
+        row.add(Root.COLUMN_AVAILABLE_BYTES, homeDir.getFreeSpace());
+        // Root.COLUMN_MIME_TYPE is another optional column and useful if you
+        // have multiple roots with different
+        // types of mime types (roots that don't match the requested mime type
+        // are automatically hidden)
+        return result;
+    }
+
+    @Override
+    public String createDocument(final String parentDocumentId, final String mimeType,
+            final String displayName) throws FileNotFoundException {
+        File newFile = new File(parentDocumentId, displayName);
+        try {
+            newFile.createNewFile();
+            return newFile.getAbsolutePath();
+        } catch (IOException e) {
+            Log.e(LocalStorageProvider.class.getSimpleName(), "Error creating new file " + newFile);
+        }
+        return null;
+    }
+
+    @Override
+    public AssetFileDescriptor openDocumentThumbnail(final String documentId, final Point sizeHint,
+            final CancellationSignal signal) throws FileNotFoundException {
+        // Assume documentId points to an image file. Build a thumbnail no
+        // larger than twice the sizeHint
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFile(documentId, options);
+        final int targetHeight = 2 * sizeHint.y;
+        final int targetWidth = 2 * sizeHint.x;
+        final int height = options.outHeight;
+        final int width = options.outWidth;
+        options.inSampleSize = 1;
+        if (height > targetHeight || width > targetWidth) {
+            final int halfHeight = height / 2;
+            final int halfWidth = width / 2;
+            // Calculate the largest inSampleSize value that is a power of 2 and
+            // keeps both
+            // height and width larger than the requested height and width.
+            while ((halfHeight / options.inSampleSize) > targetHeight
+                    || (halfWidth / options.inSampleSize) > targetWidth) {
+                options.inSampleSize *= 2;
+            }
+        }
+        options.inJustDecodeBounds = false;
+        Bitmap bitmap = BitmapFactory.decodeFile(documentId, options);
+        // Write out the thumbnail to a temporary file
+        File tempFile = null;
+        FileOutputStream out = null;
+        try {
+            tempFile = File.createTempFile("thumbnail", null, getContext().getCacheDir());
+            out = new FileOutputStream(tempFile);
+            bitmap.compress(Bitmap.CompressFormat.PNG, 90, out);
+        } catch (IOException e) {
+            Log.e(LocalStorageProvider.class.getSimpleName(), "Error writing thumbnail", e);
+            return null;
+        } finally {
+            if (out != null)
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    Log.e(LocalStorageProvider.class.getSimpleName(), "Error closing thumbnail", e);
+                }
+        }
+        // It appears the Storage Framework UI caches these results quite
+        // aggressively so there is little reason to
+        // write your own caching layer beyond what you need to return a single
+        // AssetFileDescriptor
+        return new AssetFileDescriptor(ParcelFileDescriptor.open(tempFile,
+                ParcelFileDescriptor.MODE_READ_ONLY), 0,
+                AssetFileDescriptor.UNKNOWN_LENGTH);
+    }
+
+    @Override
+    public Cursor queryChildDocuments(final String parentDocumentId, final String[] projection,
+            final String sortOrder) throws FileNotFoundException {
+        // Create a cursor with either the requested fields, or the default
+        // projection if "projection" is null.
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
+        final File parent = new File(parentDocumentId);
+        for (File file : parent.listFiles()) {
+            // Don't show hidden files/folders
+            if (!file.getName().startsWith(".")) {
+                // Adds the file's display name, MIME type, size, and so on.
+                includeFile(result, file);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocument(final String documentId, final String[] projection)
+            throws FileNotFoundException {
+        // Create a cursor with either the requested fields, or the default
+        // projection if "projection" is null.
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
+        includeFile(result, new File(documentId));
+        return result;
+    }
+
+    private void includeFile(final MatrixCursor result, final File file)
+            throws FileNotFoundException {
+        final MatrixCursor.RowBuilder row = result.newRow();
+        // These columns are required
+        row.add(Document.COLUMN_DOCUMENT_ID, file.getAbsolutePath());
+        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
+        String mimeType = getDocumentType(file.getAbsolutePath());
+        row.add(Document.COLUMN_MIME_TYPE, mimeType);
+        int flags = file.canWrite() ? Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE
+                : 0;
+        // We only show thumbnails for image files - expect a call to
+        // openDocumentThumbnail for each file that has
+        // this flag set
+        if (mimeType.startsWith("image/"))
+            flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
+        row.add(Document.COLUMN_FLAGS, flags);
+        // COLUMN_SIZE is required, but can be null
+        row.add(Document.COLUMN_SIZE, file.length());
+        // These columns are optional
+        row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
+        // Document.COLUMN_ICON can be a resource id identifying a custom icon.
+        // The system provides default icons
+        // based on mime type
+        // Document.COLUMN_SUMMARY is optional additional information about the
+        // file
+    }
+
+    @Override
+    public String getDocumentType(final String documentId) throws FileNotFoundException {
+        File file = new File(documentId);
+        if (file.isDirectory())
+            return Document.MIME_TYPE_DIR;
+        // From FileProvider.getType(Uri)
+        final int lastDot = file.getName().lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = file.getName().substring(lastDot + 1);
+            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
+            }
+        }
+        return "application/octet-stream";
+    }
+
+    @Override
+    public void deleteDocument(final String documentId) throws FileNotFoundException {
+        new File(documentId).delete();
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(final String documentId, final String mode,
+            final CancellationSignal signal) throws FileNotFoundException {
+        File file = new File(documentId);
+        final boolean isWrite = (mode.indexOf('w') != -1);
+        if (isWrite) {
+            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE);
+        } else {
+            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+}

+ 218 - 0
aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileChooserActivity.java

@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ipaulpro.afilechooser;
+
+import android.app.ActionBar;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentManager.BackStackEntry;
+import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
+import android.support.v4.app.FragmentTransaction;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import java.io.File;
+
+/**
+ * Main Activity that handles the FileListFragments
+ *
+ * @version 2013-06-25
+ * @author paulburke (ipaulpro)
+ */
+public class FileChooserActivity extends FragmentActivity implements
+        OnBackStackChangedListener, FileListFragment.Callbacks {
+
+    public static final String PATH = "path";
+    public static final String EXTERNAL_BASE_PATH = Environment
+            .getExternalStorageDirectory().getAbsolutePath();
+
+    private static final boolean HAS_ACTIONBAR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+
+    private FragmentManager mFragmentManager;
+    private BroadcastReceiver mStorageListener = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Toast.makeText(context, R.string.storage_removed, Toast.LENGTH_LONG).show();
+            finishWithResult(null);
+        }
+    };
+
+    private String mPath;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mFragmentManager = getSupportFragmentManager();
+        mFragmentManager.addOnBackStackChangedListener(this);
+
+        if (savedInstanceState == null) {
+            mPath = EXTERNAL_BASE_PATH;
+            addFragment();
+        } else {
+            mPath = savedInstanceState.getString(PATH);
+        }
+
+        setTitle(mPath);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        unregisterStorageListener();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        registerStorageListener();
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putString(PATH, mPath);
+    }
+
+    @Override
+    public void onBackStackChanged() {
+
+        int count = mFragmentManager.getBackStackEntryCount();
+        if (count > 0) {
+            BackStackEntry fragment = mFragmentManager.getBackStackEntryAt(count - 1);
+            mPath = fragment.getName();
+        } else {
+            mPath = EXTERNAL_BASE_PATH;
+        }
+
+        setTitle(mPath);
+        if (HAS_ACTIONBAR)
+            invalidateOptionsMenu();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (HAS_ACTIONBAR) {
+            boolean hasBackStack = mFragmentManager.getBackStackEntryCount() > 0;
+
+            ActionBar actionBar = getActionBar();
+            actionBar.setDisplayHomeAsUpEnabled(hasBackStack);
+            actionBar.setHomeButtonEnabled(hasBackStack);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                mFragmentManager.popBackStack();
+                return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * Add the initial Fragment with given path.
+     */
+    private void addFragment() {
+        FileListFragment fragment = FileListFragment.newInstance(mPath);
+        mFragmentManager.beginTransaction()
+                .add(android.R.id.content, fragment).commit();
+    }
+
+    /**
+     * "Replace" the existing Fragment with a new one using given path. We're
+     * really adding a Fragment to the back stack.
+     *
+     * @param file The file (directory) to display.
+     */
+    private void replaceFragment(File file) {
+        mPath = file.getAbsolutePath();
+
+        FileListFragment fragment = FileListFragment.newInstance(mPath);
+        mFragmentManager.beginTransaction()
+                .replace(android.R.id.content, fragment)
+                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
+                .addToBackStack(mPath).commit();
+    }
+
+    /**
+     * Finish this Activity with a result code and URI of the selected file.
+     *
+     * @param file The file selected.
+     */
+    private void finishWithResult(File file) {
+        if (file != null) {
+            Uri uri = Uri.fromFile(file);
+            setResult(RESULT_OK, new Intent().setData(uri));
+            finish();
+        } else {
+            setResult(RESULT_CANCELED);
+            finish();
+        }
+    }
+
+    /**
+     * Called when the user selects a File
+     *
+     * @param file The file that was selected
+     */
+    @Override
+    public void onFileSelected(File file) {
+        if (file != null) {
+            if (file.isDirectory()) {
+                replaceFragment(file);
+            } else {
+                finishWithResult(file);
+            }
+        } else {
+            Toast.makeText(FileChooserActivity.this, R.string.error_selecting_file,
+                    Toast.LENGTH_SHORT).show();
+        }
+    }
+
+    /**
+     * Register the external storage BroadcastReceiver.
+     */
+    private void registerStorageListener() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_MEDIA_REMOVED);
+        registerReceiver(mStorageListener, filter);
+    }
+
+    /**
+     * Unregister the external storage BroadcastReceiver.
+     */
+    private void unregisterStorageListener() {
+        unregisterReceiver(mStorageListener);
+    }
+}

+ 121 - 0
aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileListAdapter.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ipaulpro.afilechooser;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * List adapter for Files.
+ * 
+ * @version 2013-12-11
+ * @author paulburke (ipaulpro)
+ */
+public class FileListAdapter extends BaseAdapter {
+
+    private final static int ICON_FOLDER = R.drawable.ic_folder;
+    private final static int ICON_FILE = R.drawable.ic_file;
+
+    private final LayoutInflater mInflater;
+
+    private List<File> mData = new ArrayList<File>();
+
+    public FileListAdapter(Context context) {
+        mInflater = LayoutInflater.from(context);
+    }
+
+    public void add(File file) {
+        mData.add(file);
+        notifyDataSetChanged();
+    }
+
+    public void remove(File file) {
+        mData.remove(file);
+        notifyDataSetChanged();
+    }
+
+    public void insert(File file, int index) {
+        mData.add(index, file);
+        notifyDataSetChanged();
+    }
+
+    public void clear() {
+        mData.clear();
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public File getItem(int position) {
+        return mData.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public int getCount() {
+        return mData.size();
+    }
+
+    public List<File> getListItems() {
+        return mData;
+    }
+
+    /**
+     * Set the list items without notifying on the clear. This prevents loss of
+     * scroll position.
+     *
+     * @param data
+     */
+    public void setListItems(List<File> data) {
+        mData = data;
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        View row = convertView;
+
+        if (row == null)
+            row = mInflater.inflate(R.layout.file, parent, false);
+
+        TextView view = (TextView) row;
+
+        // Get the file at the current position
+        final File file = getItem(position);
+
+        // Set the TextView as the file name
+        view.setText(file.getName());
+
+        // If the item is not a directory, use the file icon
+        int icon = file.isDirectory() ? ICON_FOLDER : ICON_FILE;
+        view.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
+
+        return row;
+    }
+
+}

+ 136 - 0
aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileListFragment.java

@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ipaulpro.afilechooser;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.v4.app.ListFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.content.Loader;
+import android.view.View;
+import android.widget.ListView;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Fragment that displays a list of Files in a given path.
+ * 
+ * @version 2013-12-11
+ * @author paulburke (ipaulpro)
+ */
+public class FileListFragment extends ListFragment implements
+        LoaderManager.LoaderCallbacks<List<File>> {
+
+    /**
+     * Interface to listen for events.
+     */
+    public interface Callbacks {
+        /**
+         * Called when a file is selected from the list.
+         *
+         * @param file The file selected
+         */
+        public void onFileSelected(File file);
+    }
+
+    private static final int LOADER_ID = 0;
+
+    private FileListAdapter mAdapter;
+    private String mPath;
+
+    private Callbacks mListener;
+
+    /**
+     * Create a new instance with the given file path.
+     *
+     * @param path The absolute path of the file (directory) to display.
+     * @return A new Fragment with the given file path.
+     */
+    public static FileListFragment newInstance(String path) {
+        FileListFragment fragment = new FileListFragment();
+        Bundle args = new Bundle();
+        args.putString(FileChooserActivity.PATH, path);
+        fragment.setArguments(args);
+
+        return fragment;
+    }
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        try {
+            mListener = (Callbacks) activity;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(activity.toString()
+                    + " must implement FileListFragment.Callbacks");
+        }
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mAdapter = new FileListAdapter(getActivity());
+        mPath = getArguments() != null ? getArguments().getString(
+                FileChooserActivity.PATH) : Environment
+                .getExternalStorageDirectory().getAbsolutePath();
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        setEmptyText(getString(R.string.empty_directory));
+        setListAdapter(mAdapter);
+        setListShown(false);
+
+        getLoaderManager().initLoader(LOADER_ID, null, this);
+
+        super.onActivityCreated(savedInstanceState);
+    }
+
+    @Override
+    public void onListItemClick(ListView l, View v, int position, long id) {
+        FileListAdapter adapter = (FileListAdapter) l.getAdapter();
+        if (adapter != null) {
+            File file = (File) adapter.getItem(position);
+            mPath = file.getAbsolutePath();
+            mListener.onFileSelected(file);
+        }
+    }
+
+    @Override
+    public Loader<List<File>> onCreateLoader(int id, Bundle args) {
+        return new FileLoader(getActivity(), mPath);
+    }
+
+    @Override
+    public void onLoadFinished(Loader<List<File>> loader, List<File> data) {
+        mAdapter.setListItems(data);
+
+        if (isResumed())
+            setListShown(true);
+        else
+            setListShownNoAnimation(true);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<List<File>> loader) {
+        mAdapter.clear();
+    }
+}

+ 149 - 0
aFileChooser/src/main/java/com/ipaulpro/afilechooser/FileLoader.java

@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ipaulpro.afilechooser;
+
+import android.content.Context;
+import android.os.FileObserver;
+import android.support.v4.content.AsyncTaskLoader;
+
+import com.ipaulpro.afilechooser.utils.FileUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Loader that returns a list of Files in a given file path.
+ * 
+ * @version 2013-12-11
+ * @author paulburke (ipaulpro)
+ */
+public class FileLoader extends AsyncTaskLoader<List<File>> {
+
+	private static final int FILE_OBSERVER_MASK = FileObserver.CREATE
+			| FileObserver.DELETE | FileObserver.DELETE_SELF
+			| FileObserver.MOVED_FROM | FileObserver.MOVED_TO
+			| FileObserver.MODIFY | FileObserver.MOVE_SELF;
+
+	private FileObserver mFileObserver;
+
+	private List<File> mData;
+	private String mPath;
+
+	public FileLoader(Context context, String path) {
+		super(context);
+		this.mPath = path;
+	}
+
+	@Override
+	public List<File> loadInBackground() {
+
+        ArrayList<File> list = new ArrayList<File>();
+
+        // Current directory File instance
+        final File pathDir = new File(mPath);
+
+        // List file in this directory with the directory filter
+        final File[] dirs = pathDir.listFiles(FileUtils.sDirFilter);
+        if (dirs != null) {
+            // Sort the folders alphabetically
+            Arrays.sort(dirs, FileUtils.sComparator);
+            // Add each folder to the File list for the list adapter
+            for (File dir : dirs)
+                list.add(dir);
+        }
+
+        // List file in this directory with the file filter
+        final File[] files = pathDir.listFiles(FileUtils.sFileFilter);
+        if (files != null) {
+            // Sort the files alphabetically
+            Arrays.sort(files, FileUtils.sComparator);
+            // Add each file to the File list for the list adapter
+            for (File file : files)
+                list.add(file);
+        }
+
+        return list;
+	}
+
+	@Override
+	public void deliverResult(List<File> data) {
+		if (isReset()) {
+			onReleaseResources(data);
+			return;
+		}
+
+		List<File> oldData = mData;
+		mData = data;
+
+		if (isStarted())
+			super.deliverResult(data);
+
+		if (oldData != null && oldData != data)
+			onReleaseResources(oldData);
+	}
+
+	@Override
+	protected void onStartLoading() {
+		if (mData != null)
+			deliverResult(mData);
+
+		if (mFileObserver == null) {
+			mFileObserver = new FileObserver(mPath, FILE_OBSERVER_MASK) {
+				@Override
+				public void onEvent(int event, String path) {
+					onContentChanged();
+				}
+			};
+		}
+		mFileObserver.startWatching();
+
+		if (takeContentChanged() || mData == null)
+			forceLoad();
+	}
+
+	@Override
+	protected void onStopLoading() {
+		cancelLoad();
+	}
+
+	@Override
+	protected void onReset() {
+		onStopLoading();
+
+		if (mData != null) {
+			onReleaseResources(mData);
+			mData = null;
+		}
+	}
+
+	@Override
+	public void onCanceled(List<File> data) {
+		super.onCanceled(data);
+
+		onReleaseResources(data);
+	}
+
+	protected void onReleaseResources(List<File> data) {
+
+		if (mFileObserver != null) {
+			mFileObserver.stopWatching();
+			mFileObserver = null;
+		}
+	}
+}

+ 570 - 0
aFileChooser/src/main/java/com/ipaulpro/afilechooser/utils/FileUtils.java

@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.ipaulpro.afilechooser.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.ianhanniballake.localstorage.LocalStorageProvider;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.text.DecimalFormat;
+import java.util.Comparator;
+
+/**
+ * @version 2009-07-03
+ * @author Peli
+ * @version 2013-12-11
+ * @author paulburke (ipaulpro)
+ */
+public class FileUtils {
+    private FileUtils() {} //private constructor to enforce Singleton pattern
+    
+    /** TAG for log messages. */
+    static final String TAG = "FileUtils";
+    private static final boolean DEBUG = false; // Set to true to enable logging
+
+    public static final String MIME_TYPE_AUDIO = "audio/*";
+    public static final String MIME_TYPE_TEXT = "text/*";
+    public static final String MIME_TYPE_IMAGE = "image/*";
+    public static final String MIME_TYPE_VIDEO = "video/*";
+    public static final String MIME_TYPE_APP = "application/*";
+
+    public static final String HIDDEN_PREFIX = ".";
+
+    /**
+     * Gets the extension of a file name, like ".png" or ".jpg".
+     *
+     * @param uri
+     * @return Extension including the dot("."); "" if there is no extension;
+     *         null if uri was null.
+     */
+    public static String getExtension(String uri) {
+        if (uri == null) {
+            return null;
+        }
+
+        int dot = uri.lastIndexOf(".");
+        if (dot >= 0) {
+            return uri.substring(dot);
+        } else {
+            // No extension.
+            return "";
+        }
+    }
+
+    /**
+     * @return Whether the URI is a local one.
+     */
+    public static boolean isLocal(String url) {
+        if (url != null && !url.startsWith("http://") && !url.startsWith("https://")) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return True if Uri is a MediaStore Uri.
+     * @author paulburke
+     */
+    public static boolean isMediaUri(Uri uri) {
+        return "media".equalsIgnoreCase(uri.getAuthority());
+    }
+
+    /**
+     * Convert File into Uri.
+     *
+     * @param file
+     * @return uri
+     */
+    public static Uri getUri(File file) {
+        if (file != null) {
+            return Uri.fromFile(file);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the path only (without file name).
+     *
+     * @param file
+     * @return
+     */
+    public static File getPathWithoutFilename(File file) {
+        if (file != null) {
+            if (file.isDirectory()) {
+                // no file to be split off. Return everything
+                return file;
+            } else {
+                String filename = file.getName();
+                String filepath = file.getAbsolutePath();
+
+                // Construct path without file name.
+                String pathwithoutname = filepath.substring(0,
+                        filepath.length() - filename.length());
+                if (pathwithoutname.endsWith("/")) {
+                    pathwithoutname = pathwithoutname.substring(0, pathwithoutname.length() - 1);
+                }
+                return new File(pathwithoutname);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @return The MIME type for the given file.
+     */
+    public static String getMimeType(File file) {
+
+        String extension = getExtension(file.getName());
+
+        if (extension.length() > 0)
+            return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
+
+        return "application/octet-stream";
+    }
+
+    /**
+     * @return The MIME type for the give Uri.
+     */
+    public static String getMimeType(Context context, Uri uri) {
+        File file = new File(getPath(context, uri));
+        return getMimeType(file);
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is {@link LocalStorageProvider}.
+     * @author paulburke
+     */
+    public static boolean isLocalStorageDocument(Uri uri) {
+        return LocalStorageProvider.AUTHORITY.equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is ExternalStorageProvider.
+     * @author paulburke
+     */
+    public static boolean isExternalStorageDocument(Uri uri) {
+        return "com.android.externalstorage.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is DownloadsProvider.
+     * @author paulburke
+     */
+    public static boolean isDownloadsDocument(Uri uri) {
+        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is MediaProvider.
+     * @author paulburke
+     */
+    public static boolean isMediaDocument(Uri uri) {
+        return "com.android.providers.media.documents".equals(uri.getAuthority());
+    }
+
+    /**
+     * @param uri The Uri to check.
+     * @return Whether the Uri authority is Google Photos.
+     */
+    public static boolean isGooglePhotosUri(Uri uri) {
+        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
+    }
+
+    /**
+     * Get the value of the data column for this Uri. This is useful for
+     * MediaStore Uris, and other file-based ContentProviders.
+     *
+     * @param context The context.
+     * @param uri The Uri to query.
+     * @param selection (Optional) Filter used in the query.
+     * @param selectionArgs (Optional) Selection arguments used in the query.
+     * @return The value of the _data column, which is typically a file path.
+     * @author paulburke
+     */
+    public static String getDataColumn(Context context, Uri uri, String selection,
+            String[] selectionArgs) {
+
+        Cursor cursor = null;
+        final String column = "_data";
+        final String[] projection = {
+                column
+        };
+
+        try {
+            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
+                    null);
+            if (cursor != null && cursor.moveToFirst()) {
+                if (DEBUG)
+                    DatabaseUtils.dumpCursor(cursor);
+
+                final int column_index = cursor.getColumnIndexOrThrow(column);
+                return cursor.getString(column_index);
+            }
+        } finally {
+            if (cursor != null)
+                cursor.close();
+        }
+        return null;
+    }
+
+    /**
+     * Get a file path from a Uri. This will get the the path for Storage Access
+     * Framework Documents, as well as the _data field for the MediaStore and
+     * other file-based ContentProviders.<br>
+     * <br>
+     * Callers should check whether the path is local before assuming it
+     * represents a local file.
+     * 
+     * @param context The context.
+     * @param uri The Uri to query.
+     * @see #isLocal(String)
+     * @see #getFile(Context, Uri)
+     * @author paulburke
+     */
+    public static String getPath(final Context context, final Uri uri) {
+
+        if (DEBUG)
+            Log.d(TAG + " File -",
+                    "Authority: " + uri.getAuthority() +
+                            ", Fragment: " + uri.getFragment() +
+                            ", Port: " + uri.getPort() +
+                            ", Query: " + uri.getQuery() +
+                            ", Scheme: " + uri.getScheme() +
+                            ", Host: " + uri.getHost() +
+                            ", Segments: " + uri.getPathSegments().toString()
+                    );
+
+        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+
+        // DocumentProvider
+        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
+            // LocalStorageProvider
+            if (isLocalStorageDocument(uri)) {
+                // The path is the id
+                return DocumentsContract.getDocumentId(uri);
+            }
+            // ExternalStorageProvider
+            else if (isExternalStorageDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                if ("primary".equalsIgnoreCase(type)) {
+                    return Environment.getExternalStorageDirectory() + "/" + split[1];
+                }
+                else
+                {
+                    final int splitIndex = docId.indexOf(':', 1);
+                    final String tag = docId.substring(0, splitIndex);
+                    final String path = docId.substring(splitIndex + 1);
+
+                    String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag);
+                    if (nonPrimaryVolume != null)
+                    {
+                        String result = nonPrimaryVolume + "/" + path;
+                        File file = new File(result);
+                        if (file.exists() && file.canRead())
+                        {
+                            return result;
+                        }
+                    }
+                }
+                // TODO handle non-primary volumes
+            }
+            // DownloadsProvider
+            else if (isDownloadsDocument(uri)) {
+
+                final String id = DocumentsContract.getDocumentId(uri);
+                final Uri contentUri = ContentUris.withAppendedId(
+                        Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
+
+                return getDataColumn(context, contentUri, null, null);
+            }
+            // MediaProvider
+            else if (isMediaDocument(uri)) {
+                final String docId = DocumentsContract.getDocumentId(uri);
+                final String[] split = docId.split(":");
+                final String type = split[0];
+
+                Uri contentUri = null;
+                if ("image".equals(type)) {
+                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+                } else if ("video".equals(type)) {
+                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+                } else if ("audio".equals(type)) {
+                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
+                }
+
+                final String selection = "_id=?";
+                final String[] selectionArgs = new String[] {
+                        split[1]
+                };
+
+                return getDataColumn(context, contentUri, selection, selectionArgs);
+            }
+        }
+        // MediaStore (and general)
+        else if ("content".equalsIgnoreCase(uri.getScheme())) {
+
+            // Return the remote address
+            if (isGooglePhotosUri(uri))
+                return uri.getLastPathSegment();
+
+            return getDataColumn(context, uri, null, null);
+        }
+        // File
+        else if ("file".equalsIgnoreCase(uri.getScheme())) {
+            return uri.getPath();
+        }
+
+        return null;
+    }
+
+    public static String getPathToNonPrimaryVolume(Context context, String tag)
+    {
+        File[] volumes = context.getExternalCacheDirs();
+        if (volumes != null)
+        {
+            for (File volume : volumes)
+            {
+                if (volume != null)
+                {
+                    String path = volume.getAbsolutePath();
+                    if (path != null)
+                    {
+                        int index = path.indexOf(tag);
+                        if (index != -1)
+                        {
+                            return path.substring(0, index) + tag;
+                        }
+                    }
+
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Convert Uri into File, if possible.
+     *
+     * @return file A local file that the Uri was pointing to, or null if the
+     *         Uri is unsupported or pointed to a remote resource.
+     * @see #getPath(Context, Uri)
+     * @author paulburke
+     */
+    public static File getFile(Context context, Uri uri) {
+        if (uri != null) {
+            String path = getPath(context, uri);
+            if (path != null && isLocal(path)) {
+                return new File(path);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the file size in a human-readable string.
+     *
+     * @param size
+     * @return
+     * @author paulburke
+     */
+    public static String getReadableFileSize(int size) {
+        final int BYTES_IN_KILOBYTES = 1024;
+        final DecimalFormat dec = new DecimalFormat("###.#");
+        final String KILOBYTES = " KB";
+        final String MEGABYTES = " MB";
+        final String GIGABYTES = " GB";
+        float fileSize = 0;
+        String suffix = KILOBYTES;
+
+        if (size > BYTES_IN_KILOBYTES) {
+            fileSize = size / BYTES_IN_KILOBYTES;
+            if (fileSize > BYTES_IN_KILOBYTES) {
+                fileSize = fileSize / BYTES_IN_KILOBYTES;
+                if (fileSize > BYTES_IN_KILOBYTES) {
+                    fileSize = fileSize / BYTES_IN_KILOBYTES;
+                    suffix = GIGABYTES;
+                } else {
+                    suffix = MEGABYTES;
+                }
+            }
+        }
+        return String.valueOf(dec.format(fileSize) + suffix);
+    }
+
+    /**
+     * Attempt to retrieve the thumbnail of given File from the MediaStore. This
+     * should not be called on the UI thread.
+     *
+     * @param context
+     * @param file
+     * @return
+     * @author paulburke
+     */
+    public static Bitmap getThumbnail(Context context, File file) {
+        return getThumbnail(context, getUri(file), getMimeType(file));
+    }
+
+    /**
+     * Attempt to retrieve the thumbnail of given Uri from the MediaStore. This
+     * should not be called on the UI thread.
+     *
+     * @param context
+     * @param uri
+     * @return
+     * @author paulburke
+     */
+    public static Bitmap getThumbnail(Context context, Uri uri) {
+        return getThumbnail(context, uri, getMimeType(context, uri));
+    }
+
+    /**
+     * Attempt to retrieve the thumbnail of given Uri from the MediaStore. This
+     * should not be called on the UI thread.
+     *
+     * @param context
+     * @param uri
+     * @param mimeType
+     * @return
+     * @author paulburke
+     */
+    public static Bitmap getThumbnail(Context context, Uri uri, String mimeType) {
+        if (DEBUG)
+            Log.d(TAG, "Attempting to get thumbnail");
+
+        if (!isMediaUri(uri)) {
+            Log.e(TAG, "You can only retrieve thumbnails for images and videos.");
+            return null;
+        }
+
+        Bitmap bm = null;
+        if (uri != null) {
+            final ContentResolver resolver = context.getContentResolver();
+            Cursor cursor = null;
+            try {
+                cursor = resolver.query(uri, null, null, null, null);
+                if (cursor.moveToFirst()) {
+                    final int id = cursor.getInt(0);
+                    if (DEBUG)
+                        Log.d(TAG, "Got thumb ID: " + id);
+
+                    if (mimeType.contains("video")) {
+                        bm = MediaStore.Video.Thumbnails.getThumbnail(
+                                resolver,
+                                id,
+                                MediaStore.Video.Thumbnails.MINI_KIND,
+                                null);
+                    }
+                    else if (mimeType.contains(FileUtils.MIME_TYPE_IMAGE)) {
+                        bm = MediaStore.Images.Thumbnails.getThumbnail(
+                                resolver,
+                                id,
+                                MediaStore.Images.Thumbnails.MINI_KIND,
+                                null);
+                    }
+                }
+            } catch (Exception e) {
+                if (DEBUG)
+                    Log.e(TAG, "getThumbnail", e);
+            } finally {
+                if (cursor != null)
+                    cursor.close();
+            }
+        }
+        return bm;
+    }
+
+    /**
+     * File and folder comparator. TODO Expose sorting option method
+     *
+     * @author paulburke
+     */
+    public static Comparator<File> sComparator = new Comparator<File>() {
+        @Override
+        public int compare(File f1, File f2) {
+            // Sort alphabetically by lower case, which is much cleaner
+            return f1.getName().toLowerCase().compareTo(
+                    f2.getName().toLowerCase());
+        }
+    };
+
+    /**
+     * File (not directories) filter.
+     *
+     * @author paulburke
+     */
+    public static FileFilter sFileFilter = new FileFilter() {
+        @Override
+        public boolean accept(File file) {
+            final String fileName = file.getName();
+            // Return files only (not directories) and skip hidden files
+            return file.isFile() && !fileName.startsWith(HIDDEN_PREFIX);
+        }
+    };
+
+    /**
+     * Folder (directories) filter.
+     *
+     * @author paulburke
+     */
+    public static FileFilter sDirFilter = new FileFilter() {
+        @Override
+        public boolean accept(File file) {
+            final String fileName = file.getName();
+            // Return directories only and skip hidden directories
+            return file.isDirectory() && !fileName.startsWith(HIDDEN_PREFIX);
+        }
+    };
+
+    /**
+     * Get the Intent for selecting content to be used in an Intent Chooser.
+     *
+     * @return The intent for opening a file with Intent.createChooser()
+     * @author paulburke
+     */
+    public static Intent createGetContentIntent() {
+        // Implicitly allow the user to select a particular kind of data
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        // The MIME data type filter
+        intent.setType("*/*");
+        // Only return URIs that can be opened with ContentResolver
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        return intent;
+    }
+}

BIN
aFileChooser/src/main/res/drawable-hdpi/ic_chooser.png


BIN
aFileChooser/src/main/res/drawable-hdpi/ic_file.png


BIN
aFileChooser/src/main/res/drawable-hdpi/ic_folder.png


BIN
aFileChooser/src/main/res/drawable-hdpi/ic_provider.png


BIN
aFileChooser/src/main/res/drawable-mdpi/ic_chooser.png


BIN
aFileChooser/src/main/res/drawable-mdpi/ic_file.png


BIN
aFileChooser/src/main/res/drawable-mdpi/ic_folder.png


BIN
aFileChooser/src/main/res/drawable-mdpi/ic_provider.png


BIN
aFileChooser/src/main/res/drawable-xhdpi/ic_chooser.png


BIN
aFileChooser/src/main/res/drawable-xhdpi/ic_file.png


BIN
aFileChooser/src/main/res/drawable-xhdpi/ic_folder.png


BIN
aFileChooser/src/main/res/drawable-xhdpi/ic_provider.png


BIN
aFileChooser/src/main/res/drawable-xxhdpi/ic_chooser.png


BIN
aFileChooser/src/main/res/drawable-xxhdpi/ic_file.png


BIN
aFileChooser/src/main/res/drawable-xxhdpi/ic_folder.png


BIN
aFileChooser/src/main/res/drawable-xxhdpi/ic_provider.png


+ 20 - 0
aFileChooser/src/main/res/layout/file.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/fileChooserName"
+    android:layout_width="fill_parent"
+    android:layout_height="?android:attr/listPreferredItemHeight" />

+ 8 - 0
aFileChooser/src/main/res/values-ca/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Carpeta buida</string>
+  <string name="storage_removed">S\'ha tret o desmuntat l\'emmagatzematge.</string>
+  <string name="choose_file">Seleccioneu un fitxer</string>
+  <string name="error_selecting_file">Error en seleccionar el fitxer</string>
+</resources>

+ 7 - 0
aFileChooser/src/main/res/values-de/strings.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="empty_directory">Leerer Ordner</string>
+    <string name="storage_removed">Speicher wurde entferntet.</string>
+    <string name="choose_file">Wähle eine Datei</string>
+    <string name="error_selecting_file">Fehler beim Öffnen der Datei</string>    
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-es/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Directorio vacío</string>
+  <string name="storage_removed">Se ha retirado o desmontado el almacenamiento.</string>
+  <string name="choose_file">Seleccione un archivo</string>
+  <string name="error_selecting_file">Error al seleccionar el archivo</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-fr/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Dossier vide</string>
+  <string name="storage_removed">Le stockage a été enlevé ou démonté.</string>
+  <string name="choose_file">Sélectionnez un fichier</string>
+  <string name="error_selecting_file">Erreur lors de la sélection du fichier</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-ga/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Comhadlann fholamh</string>
+  <string name="storage_removed">Baineadh amach an gléas stórála nó dínascadh é.</string>
+  <string name="choose_file">Roghnaigh comhad</string>
+  <string name="error_selecting_file">Tharla botún fad is a bhí comhad á roghnú</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-it/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Directory vuota</string>
+  <string name="storage_removed">Lo spazio di archiviazione è stato rimosso o smontato.</string>
+  <string name="choose_file">Selezionare un file</string>
+  <string name="error_selecting_file">Errore nel selezionare il File</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-ko/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by @booknara -->
+<resources>
+  <string name="empty_directory">빈 디렉토리</string>
+  <string name="storage_removed">저장소가 제거되었습니다.</string>
+  <string name="choose_file">파일 선택</string>
+  <string name="error_selecting_file">파일 선택 오류</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-pl/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Pusty katalog</string>
+  <string name="storage_removed">Pamięć została usunięta lub odmontowana.</string>
+  <string name="choose_file">Wybierz plik</string>
+  <string name="error_selecting_file">Błąd, podczas wybierania pliku</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-pt-rBR/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Pasta Vazia</string>
+  <string name="storage_removed">Unidade externa removida ou não preparada.</string>
+  <string name="choose_file">Selecione um Arquivo</string>
+  <string name="error_selecting_file">Erro ao selecionar o Arquivo</string>
+</resources>

+ 8 - 0
aFileChooser/src/main/res/values-ru/strings.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--Generated by crowdin.net-->
+<resources>
+  <string name="empty_directory">Пустая папка</string>
+  <string name="storage_removed">Storage was removed or unmounted.</string>
+  <string name="choose_file">Выберите файл</string>
+  <string name="error_selecting_file">Ошибка при выборе файла</string>
+</resources>

+ 19 - 0
aFileChooser/src/main/res/values-v11/strings.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+<resources>
+    <string name="choose_file">Choose a file</string>    
+</resources>

+ 7 - 0
aFileChooser/src/main/res/values-v19/bool.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <bool name="use_activity">false</bool>
+    <bool name="use_provider">true</bool>
+
+</resources>

+ 7 - 0
aFileChooser/src/main/res/values/bool.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <bool name="use_activity">true</bool>
+    <bool name="use_provider">false</bool>
+
+</resources>

+ 20 - 0
aFileChooser/src/main/res/values/dimens.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2012 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+	<dimen name="list_padding">0dp</dimen>
+	<dimen name="list_item_padding">16dp</dimen>
+</resources>

+ 23 - 0
aFileChooser/src/main/res/values/strings.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 
+ * Copyright (C) 2012 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+<resources>
+    <string name="empty_directory">Empty Directory</string>
+    <string name="storage_removed">Storage was removed or unmounted.</string>
+    <string name="choose_file">Select a file</string>
+    <string name="error_selecting_file">Error selecting File</string>
+    <string name="internal_storage" translatable="false">Internal storage</string>
+</resources>

+ 29 - 0
aFileChooser/src/main/res/values/styles.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2013 Paul Burke
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="fileChooserName">
+        <item name="android:drawablePadding">@dimen/list_item_padding</item>
+        <item name="android:ellipsize">end</item>
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:maxLines">2</item>
+        <item name="android:paddingLeft">@dimen/list_item_padding</item>
+        <item name="android:paddingRight">@dimen/list_item_padding</item>
+        <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
+    </style>
+
+</resources>

+ 89 - 0
aFileChooser/src/main/res/xml/mimetypes.xml

@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 
+ * Copyright (C) 2007-2008 OpenIntents.org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ -->
+<MimeTypes>
+	<!-- Image types -->
+	<type extension=".png" mimetype="image/png" />
+	<type extension=".gif" mimetype="image/gif" />
+	<type extension=".jpg" mimetype="image/jpeg" />
+	<type extension=".jpeg" mimetype="image/jpeg" />
+	<type extension=".bmp" mimetype="image/bmp" />
+	<type extension=".tiff" mimetype="image/tiff" />
+	<type extension=".tif" mimetype="image/tiff" />
+	<type extension=".icon" mimetype="image/x-icon" />
+	    
+	<!-- Audio types -->
+	<type extension=".mp3" mimetype="audio/mpeg" />
+	<type extension=".mp2" mimetype="audio/mpeg" />
+	<type extension=".mpga" mimetype="audio/mpeg" />
+	<type extension=".m4a" mimetype="audio/mp4a-latm" />
+	<type extension=".m4p" mimetype="audio/mp4a-latm" />
+	<type extension=".wav" mimetype="audio/wav" />
+	<type extension=".ogg" mimetype="audio/x-ogg" />
+	<type extension=".mid" mimetype="audio/mid" />
+	<type extension=".midi" mimetype="audio/midi" />
+	<type extension=".amr" mimetype="audio/AMR" />
+	<type extension=".aac" mimetype="audio/x-aac"/>
+	<type extension=".m3u" mimetype="audio/x-mpegurl"/>
+	<type extension=".ram" mimetype="audio/x-pn-realaudio"/>
+	<type extension=".ra" mimetype="audio/x-pn-realaudio"/>
+	<type extension=".aif" mimetype="audio/x-aiff"/>
+	<type extension=".aiff" mimetype="audio/x-aiff"/>
+	<type extension=".aifc" mimetype="audio/x-aiff"/>
+			
+	<!-- Video types -->
+	<type extension=".mpeg" mimetype="video/mpeg" />
+	<type extension=".mpg" mimetype="video/mpeg" />	
+	<type extension=".mpe" mimetype="video/mpeg" />
+	<type extension=".mov" mimetype="video/quicktime" />
+	<type extension=".qt" mimetype="video/quicktime" />
+	<type extension=".mp4" mimetype="video/mpeg" />
+	<type extension=".3gp" mimetype="video/3gpp" />
+	<type extension=".3gpp" mimetype="video/3gpp" />
+	<type extension=".m4u" mimetype="video/vnd.mpegurl" />
+	<type extension=".mxu" mimetype="video/vnd.mpegurl" />
+	<type extension=".flv" mimetype="video/x-flv" />
+	<type extension=".wmx" mimetype="video/x-ms-wmv" />
+	<type extension=".avi" mimetype="video/x-msvideo" />
+			
+	<!-- Package types -->
+	<type extension=".jar" mimetype="application/java-archive" />
+	<type extension=".zip" mimetype="application/zip" />
+	<type extension=".rar" mimetype="application/x-rar-compressed" />
+	<type extension=".gz" mimetype="application/gzip" />
+	
+	<!-- Web browser types -->
+	<type extension=".htm" mimetype="text/html" />
+	<type extension=".html" mimetype="text/html" />
+	<type extension=".php" mimetype="text/php " />
+	
+	<!-- Doc types -->
+	<type extension=".txt" mimetype="text/plain" />
+	<type extension=".rtf" mimetype="text/rtf" />
+	<type extension=".csv" mimetype="text/csv" />
+	<type extension=".xml" mimetype="text/xml" />
+	<type extension=".xml" mimetype="text/xml" />
+	<type extension=".css" mimetype="text/css" />
+	<type extension=".doc" mimetype="application/msword" />
+	<type extension=".docx" mimetype="application/msword" />
+	<type extension=".ppt" mimetype="application/vnd.ms-powerpoint" />
+	<type extension=".pdf" mimetype="application/pdf" />
+	<type extension=".xls" mimetype="application/vnd.ms-excel" />
+	
+	<!-- Android specific -->
+	<type extension=".apk" mimetype="application/vnd.android.package-archive" />
+	
+</MimeTypes>

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 31 - 0
app/build.gradle

@@ -0,0 +1,31 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 27
+    defaultConfig {
+        applicationId "de.tu_darmstadt.informatik.tk.olir"
+        minSdkVersion 15
+        targetSdkVersion 27
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation 'com.android.support:design:27.1.0'
+    compile project(':aFileChooser')
+    compile fileTree(include: ['*.jar'], dir: 'libs')
+    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+        exclude group: 'com.android.support', module: 'support-annotations'
+    })
+    compile 'com.android.support:appcompat-v7:27.1.0'
+    compile 'com.android.support.constraint:constraint-layout:1.0.2'
+    testCompile 'junit:junit:4.12'
+}

+ 25 - 0
app/proguard-rules.pro

@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Users\HairBears\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
app/src/androidTest/java/de/tu_darmstadt/informatik/tk/olir/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("de.tu_darmstadt.informatik.tk.olir", appContext.getPackageName());
+    }
+}

+ 44 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="de.tu_darmstadt.informatik.tk.olir">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
+        <activity
+            android:name=".MainActivity"
+            android:label="Previous Objects">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name="com.ipaulpro.afilechooser.FileChooserActivity"
+            android:enabled="@bool/use_activity"
+            android:exported="true"
+            android:icon="@drawable/ic_chooser"
+            android:label="@string/choose_file">
+            <intent-filter>
+                <action android:name="android.intent.action.GET_CONTENT" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.OPENABLE" />
+
+                <data android:mimeType="*/*" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".ObjectsActivity"
+            android:label="@string/title_activity_objects" />
+        <activity android:name=".ReadingActivity"></activity>
+    </application>
+
+</manifest>

+ 133 - 0
app/src/main/java/de/tu_darmstadt/informatik/tk/olir/DrawView.java

@@ -0,0 +1,133 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Area where the object's shape gets drawn
+ * Created by Martin Herbers on 30.03.2017.
+ */
+
+
+public class DrawView extends View {
+
+    /**
+     * List of Strings where every String describes a basic shape, e.g. rectangle: [R;0;0;20;20]
+     */
+    ArrayList<String> shapes;
+    /**
+     * millimeter * scale = pixel
+     */
+    float scale;
+
+    public DrawView(Context context) {
+        super(context);
+        clearShapes();
+    }
+
+    public DrawView(Context context, AttributeSet attribs) {
+        super(context, attribs);
+        clearShapes();
+    }
+
+    public DrawView(Context context, AttributeSet attribs, int defStyle) {
+        super(context, attribs, defStyle);
+        clearShapes();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+
+        //Middle point of the draw area
+        int baseX = this.getMeasuredHeight() / 2;
+        int baseY = baseX;
+
+
+        Paint paint = new Paint();
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(Color.LTGRAY);
+        for (String s: shapes) {
+            //Circle
+            if (s.charAt(1) == 'C') {
+                //X offset from middle point
+                s = s.substring(3);
+                int index = s.indexOf(';');
+                float x = Float.parseFloat(s.substring(0, index));
+                //Y offset from middle point
+                s = s.substring(index + 1);
+                index = s.indexOf(';');
+                float y = Float.parseFloat(s.substring(0, index));
+                //the circle's radius
+                s = s.substring(index + 1);
+                index = s.length()-1;
+                float radius = Float.parseFloat(s.substring(0, index)) / 2;
+
+                //Draw the circle shape
+                canvas.drawCircle(baseX + x * scale,
+                        baseY + y * scale,
+                        radius * scale,
+                        paint);
+
+                //Rectangle
+            } else if (s.charAt(1) == 'R') {
+                //X offset from middle point
+                s = s.substring(3);
+                int index = s.indexOf(';');
+                float x = Float.parseFloat(s.substring(0, index));
+                //Y offset from middle point
+                s = s.substring(index + 1);
+                index = s.indexOf(';');
+                float y = Float.parseFloat(s.substring(0, index));
+                //width
+                s = s.substring(index + 1);
+                index = s.indexOf(';');
+                float width = Float.parseFloat(s.substring(0, index));
+                //height
+                s = s.substring(index + 1);
+                index = s.length()-1;
+                float height = Float.parseFloat(s.substring(0, index));
+
+                //Draw the rectangle
+                canvas.drawRect(baseX + x * scale - width / 2 * scale,
+                            baseY + y * scale - height / 2 * scale,
+                            baseX + x * scale + width / 2 * scale,
+                            baseY + y * scale + height / 2 * scale,
+                            paint);
+            }
+        }
+    }
+
+
+    /**
+     * Clear the shapes list, use it every time a new object is loaded
+     */
+    public void clearShapes() {
+        shapes = new ArrayList<>();
+    }
+
+    /**
+     * Add a shape to the list
+     * @param s     the string describing a basic shape
+     */
+    public void addShape(String s) {
+        shapes.add(s);
+    }
+
+    /**
+     * Set the scale to calculate millimeter to pixel
+     * @param s
+     */
+    public void setScale(float s) {
+        scale = s;
+    }
+
+}

+ 71 - 0
app/src/main/java/de/tu_darmstadt/informatik/tk/olir/MainActivity.java

@@ -0,0 +1,71 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.Manifest;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.net.Uri;
+import android.support.constraint.ConstraintLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.ipaulpro.afilechooser.utils.FileUtils;
+
+import java.io.File;
+import java.net.URISyntaxException;
+
+public class MainActivity extends AppCompatActivity {
+
+
+    public static String stringFalse = "";
+    public static String stringTrue = "";
+    private static DrawView drawView;
+    private float scale;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+
+
+        //FROM http://stackoverflow.com/questions/8656968/convert-mm-to-pixels 17.3.17 16.30
+        scale = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
+                getResources().getDisplayMetrics());
+
+        /*drawView = (DrawView) this.findViewById(R.id.drawView);
+        drawView.setScale(scale);
+        drawView.setBackgroundColor(Color.WHITE);
+        this.findViewById(R.id.button2).setOnTouchListener(new View.OnTouchListener() {
+
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                if (event.getAction() == MotionEvent.ACTION_DOWN ) {
+                    changeText(v);
+                    return true;
+                }
+                return false;
+            }
+        });*/
+    }
+
+    public void addObject(View view) {
+        Intent intent = new Intent(this, ObjectsActivity.class);
+        startActivity(intent);
+    }
+
+
+
+
+    public void changeText(View view) {
+        /*TextView text = (TextView)this.findViewById(R.id.textView2);
+        text.setText(stringTrue);
+        text.setTextColor(Color.RED);*/
+    }
+}

+ 125 - 0
app/src/main/java/de/tu_darmstadt/informatik/tk/olir/ObjectsActivity.java

@@ -0,0 +1,125 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.ipaulpro.afilechooser.utils.FileUtils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+public class ObjectsActivity extends AppCompatActivity {
+
+    static final int FILE_SELECT_CODE = 1;
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_objects);
+
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            Intent intent = new Intent(this, MainActivity.class);
+            startActivity(intent);
+            return true;
+        }
+        return false;
+    }
+
+    public void openFile(View view) {
+        // Create the ACTION_GET_CONTENT Intent
+        Intent getContentIntent = FileUtils.createGetContentIntent();
+
+        Intent intent = Intent.createChooser(getContentIntent, "1. Select a file");
+        startActivityForResult(intent, FILE_SELECT_CODE);
+    }
+
+    //FROM http://stackoverflow.com/questions/7856959/android-file-chooser 14.3.17 13:00
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case FILE_SELECT_CODE:
+                if (resultCode == RESULT_OK) {
+                    // Get the Uri of the selected file
+                    Uri uri = data.getData();
+                    // Get the path
+                    String path = FileUtils.getPath(this, uri);
+                    if (path != null && FileUtils.isLocal(path)) {
+                        File file = new File(path);
+                        try {
+                            openReadingActivityWith(new FileInputStream(file));
+                        } catch (FileNotFoundException e) {
+                            e.printStackTrace();
+                        }
+
+                      /*  Parser.parseFile(file, drawView, (TextView) this.findViewById(R.id.textView), (Button) this.findViewById(R.id.button2), scale);
+                        TextView text = (TextView)this.findViewById(R.id.textView2);
+                        text.setText(stringFalse);
+                        text.setTextColor(Color.BLACK);*/
+                    }
+                }
+                break;
+        }
+        super.onActivityResult(requestCode, resultCode, data);
+    }
+
+
+    public void openLoad(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_load));
+    }
+
+    public void openPressure(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_pressure));
+    }
+
+    public void openAcceleration(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_acceleration));
+    }
+
+    public void openTilt90(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_tilt90));
+    }
+
+    public void openTilt180(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_tilt180));
+
+    }
+
+    public void openTempRising(View view) {
+        openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_temp_rising));
+    }
+
+    public void openTempFalling(View view) {
+       openReadingActivityWith(getResources().openRawResource(R.raw.object_specs_temp_falling));
+    }
+
+    public void openReadingActivityWith(InputStream stream) {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+        String[] content = new String[5];
+        String line;
+        int i = 0;
+        try {
+            while ((line = reader.readLine()) != null)  {
+                content[i++] = line;
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        Intent intent = new Intent(this, ReadingActivity.class);
+        intent.putExtra("file", content);
+        startActivity(intent);
+    }
+}

+ 121 - 0
app/src/main/java/de/tu_darmstadt/informatik/tk/olir/Parser.java

@@ -0,0 +1,121 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.support.constraint.ConstraintLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Parses an objectSpecs.txt file
+ * Created by Martin Herbers on 17.03.2017.
+ */
+
+public class Parser {
+
+    //Save the first button position as the middle point and use it to reposition the button
+    private static boolean first = true;
+    private static int baseX;
+    private static int baseY;
+
+
+    public static void parseFile(String[] content, AppCompatActivity activity) {
+        //First line: Object title
+        TextView textView = activity.findViewById(R.id.textView);
+        textView.setText(content[0].substring(content[0].indexOf(':')+1));
+        //Set the two status strings
+        //TODO MainActivity.stringTrue = list.get(3).substring(list.get(3).indexOf(':')+1);
+        //TODO MainActivity.stringFalse = list.get(4).substring(list.get(4).indexOf(':')+1);
+
+        DrawView background = activity.findViewById(R.id.drawView);
+        //Outer shapes
+        String shapes = content[1].substring(content[1].indexOf(':')+1);
+        background.clearShapes();
+        int idx = shapes.indexOf(']')+1;
+        while (idx > 0) {
+            //Add every basic shape to the list that gets drawn
+            background.addShape(shapes.substring(0, idx));
+            shapes = shapes.substring(idx);
+            idx = shapes.indexOf(']')+1;
+        }
+
+        background.invalidate();
+
+        Button button = activity.findViewById(R.id.button);
+        if (first) {
+            baseX = (int) button.getX();
+            baseY = (int) button.getY();
+            first = false;
+        }
+
+        //Button size and position
+        String buttonText = content[2].substring(content[2].indexOf(':')+1);
+        float scale = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
+                activity.getResources().getDisplayMetrics());
+        if (buttonText.charAt(1) == 'C') {
+
+            buttonText = buttonText.substring(3);
+            int index = buttonText.indexOf(';');
+            float x = Float.parseFloat(buttonText.substring(0, index));
+
+            buttonText = buttonText.substring(index + 1);
+            index = buttonText.indexOf(';');
+            float y = Float.parseFloat(buttonText.substring(0, index));
+
+            buttonText = buttonText.substring(index + 1);
+            index = buttonText.length()-1;
+            float diameter = Float.parseFloat(buttonText.substring(0, index));
+
+            int dia = (int) (diameter * scale) ;
+
+            ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) button.getLayoutParams();
+            layoutParams.horizontalBias = (baseX + x * scale) / (baseX * 2);
+            layoutParams.verticalBias = (baseY + y * scale) / (baseY * 2);
+            layoutParams.width = dia;
+            layoutParams.height = dia;
+            button.setLayoutParams(layoutParams);
+
+        } else if (buttonText.charAt(1) == 'R') {
+
+            buttonText = buttonText.substring(3);
+            int index = buttonText.indexOf(';');
+            float x = Float.parseFloat(buttonText.substring(0, index));
+
+            buttonText = buttonText.substring(index + 1);
+            index = buttonText.indexOf(';');
+            float y = Float.parseFloat(buttonText.substring(0, index));
+
+            buttonText = buttonText.substring(index + 1);
+            index = buttonText.indexOf(';');
+            float width = Float.parseFloat(buttonText.substring(0, index));
+
+            buttonText = buttonText.substring(index + 1);
+            index = buttonText.length()-1;
+            float height = Float.parseFloat(buttonText.substring(0, index));
+
+            int w = (int) (width * scale);
+            int h = (int) (height * scale);
+
+            ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) button.getLayoutParams();
+            layoutParams.horizontalBias = (baseX + x * scale) / (baseX * 2);
+            layoutParams.verticalBias = (baseY + y * scale) / (baseY * 2);
+            layoutParams.width = w;
+            layoutParams.height = h;
+            button.setLayoutParams(layoutParams);
+        }
+    }
+}

+ 44 - 0
app/src/main/java/de/tu_darmstadt/informatik/tk/olir/ReadingActivity.java

@@ -0,0 +1,44 @@
+package de.tu_darmstadt.informatik.tk.olir;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+
+public class ReadingActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_reading);
+
+        String[] file = getIntent().getStringArrayExtra("file");
+
+        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        getSupportActionBar().setTitle("2. Put the Object onto the marked Area");
+
+        Parser.parseFile(file, this);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)  {
+        if (keyCode == KeyEvent.KEYCODE_BACK
+                && event.getRepeatCount() == 0) {
+            Intent intent = new Intent(this, MainActivity.class);
+            startActivity(intent);
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            Intent intent = new Intent(this, MainActivity.class);
+            startActivity(intent);
+            return true;
+        }
+        return false;
+    }
+}

+ 45 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="de.tu_darmstadt.informatik.tk.olir.MainActivity"
+    tools:layout_editor_absoluteX="0dp"
+    tools:layout_editor_absoluteY="81dp">
+
+
+    <TableLayout
+        android:id="@+id/tableLayout"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="1.0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.0">
+
+    </TableLayout>
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/floatingActionButton2"
+        android:layout_width="75dp"
+        android:layout_height="217dp"
+        android:layout_marginBottom="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:clickable="true"
+        android:onClick="addObject"
+        app:backgroundTint="@color/colorPrimary"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.92"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.92"
+        app:srcCompat="@mipmap/plus" />
+
+</android.support.constraint.ConstraintLayout>

+ 306 - 0
app/src/main/res/layout/activity_objects.xml

@@ -0,0 +1,306 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="de.tu_darmstadt.informatik.tk.olir.ObjectsActivity">
+
+    <TableLayout
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHorizontal_bias="0.0"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.0">
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openAcceleration"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView3"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/acceleration" />
+
+            <TextView
+                android:id="@+id/textView3"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Acceleration"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line1"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"
+                android:background="#FF909090"
+                android:padding="2dip" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="50dp"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openLoad"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/weight" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Load"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line1"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"
+                android:background="#FF909090"
+                android:padding="2dip" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openTempRising"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView6"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/temperature" />
+
+            <TextView
+                android:id="@+id/textView6"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Rising Temperature"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line2"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"
+                android:background="#FF909090"
+                android:padding="2dip" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openTempFalling"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView7"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/temperature" />
+
+            <TextView
+                android:id="@+id/textView7"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Falling Temperature"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line3"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"
+                android:background="#FF909090"
+                android:padding="2dip" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openTilt90"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView4"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/rotation" />
+
+            <TextView
+                android:id="@+id/textView4"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Tilting (90°)"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line4"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"
+                android:background="#FF909090"
+                android:padding="2dip" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:gravity="left|center_vertical"
+            android:minHeight="50dp"
+            android:onClick="openTilt180"
+            android:padding="10dp">
+
+            <ImageView
+                android:id="@+id/imageView5"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:src="@mipmap/rotation" />
+
+            <TextView
+                android:id="@+id/textView5"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="5dp"
+                android:text="Tilting (180°)"
+                android:textSize="20sp" />
+        </TableRow>
+
+        <TableRow
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+
+            <View
+                android:id="@+id/line5"
+                android:layout_width="match_parent"
+                android:layout_height="1dip"
+                android:layout_weight="1"