diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..3cf9e98d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,54 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+#
+# DD-WRT Companion is a mobile app that lets you connect to,
+# monitor and manage your DD-WRT routers on the go.
+#
+# Copyright (C) 2014 Armel Soro
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Contact Info: Armel Soro <apps+ddwrt AT rm3l DOT org>
+#
+
+**/*.class
+
+# Mobile Tools for Java (J2ME)
+**/.mtj.tmp/
+
+libraries/**/build
+build
+.gradle
+
+**/*.apk
+
+
+samples
+#DEPLOYMENT
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+**/hs_err_pid*
+.idea
+actionbarsherlock
+**/*.iml
+local.properties
+
+
+.DS_Store
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..03bced9f
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.1.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..1d3591c8
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..122a0dca
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..9d82f789
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/maoni-sample/.gitignore b/maoni-sample/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/maoni-sample/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/maoni-sample/build.gradle b/maoni-sample/build.gradle
new file mode 100644
index 00000000..7b722c45
--- /dev/null
+++ b/maoni-sample/build.gradle
@@ -0,0 +1,27 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.2"
+
+ defaultConfig {
+ applicationId "org.rm3l.maoni"
+ minSdkVersion 14
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile project(":maoni")
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ testCompile 'junit:junit:4.12'
+ compile 'com.android.support:appcompat-v7:23.3.0'
+}
diff --git a/maoni-sample/proguard-rules.pro b/maoni-sample/proguard-rules.pro
new file mode 100644
index 00000000..88116af3
--- /dev/null
+++ b/maoni-sample/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/rm3l/Library/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 *;
+#}
diff --git a/maoni-sample/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java b/maoni-sample/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java
new file mode 100644
index 00000000..acf3cb6a
--- /dev/null
+++ b/maoni-sample/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.rm3l.maoni;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/maoni-sample/src/main/AndroidManifest.xml b/maoni-sample/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..33b8a074
--- /dev/null
+++ b/maoni-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/maoni-sample/src/main/java/org/rm3l/maoni/MainActivity.java b/maoni-sample/src/main/java/org/rm3l/maoni/MainActivity.java
new file mode 100644
index 00000000..bc52896e
--- /dev/null
+++ b/maoni-sample/src/main/java/org/rm3l/maoni/MainActivity.java
@@ -0,0 +1,37 @@
+package org.rm3l.maoni;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * Created by rm3l on 08/05/16.
+ */
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_maoni_sample);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ //TODO
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/maoni-sample/src/main/java/org/rm3l/maoni/MaoniSampleMainActivity.java b/maoni-sample/src/main/java/org/rm3l/maoni/MaoniSampleMainActivity.java
new file mode 100644
index 00000000..c18d6232
--- /dev/null
+++ b/maoni-sample/src/main/java/org/rm3l/maoni/MaoniSampleMainActivity.java
@@ -0,0 +1,29 @@
+package org.rm3l.maoni;
+
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.Snackbar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+
+public class MaoniSampleMainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_maoni_sample_main);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.maoni_toolbar);
+ setSupportActionBar(toolbar);
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.maoni_fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ new Maoni(MyFeedbackActivity.class)
+ .start(MaoniSampleMainActivity.this);
+ }
+ });
+ }
+
+}
diff --git a/maoni-sample/src/main/java/org/rm3l/maoni/MyFeedbackActivity.java b/maoni-sample/src/main/java/org/rm3l/maoni/MyFeedbackActivity.java
new file mode 100644
index 00000000..68512b8d
--- /dev/null
+++ b/maoni-sample/src/main/java/org/rm3l/maoni/MyFeedbackActivity.java
@@ -0,0 +1,26 @@
+package org.rm3l.maoni;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.FragmentActivity;
+import android.widget.Toast;
+
+import org.rm3l.maoni.model.Feedback;
+import org.rm3l.maoni.ui.MaoniActivity;
+
+/**
+ * Created by rm3l on 08/05/16.
+ */
+public class MyFeedbackActivity extends MaoniActivity {
+
+
+ @Override
+ protected void onDismiss() {
+ Toast.makeText(MyFeedbackActivity.this, "onDismiss", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onSendButtonClicked(@NonNull Feedback feedback) {
+
+ }
+}
diff --git a/maoni-sample/src/main/res/layout/activity_maoni_sample.xml b/maoni-sample/src/main/res/layout/activity_maoni_sample.xml
new file mode 100644
index 00000000..f9504c9a
--- /dev/null
+++ b/maoni-sample/src/main/res/layout/activity_maoni_sample.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/maoni-sample/src/main/res/layout/activity_maoni_sample_main.xml b/maoni-sample/src/main/res/layout/activity_maoni_sample_main.xml
new file mode 100644
index 00000000..4c24d4bd
--- /dev/null
+++ b/maoni-sample/src/main/res/layout/activity_maoni_sample_main.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maoni-sample/src/main/res/layout/content_maoni_sample_main.xml b/maoni-sample/src/main/res/layout/content_maoni_sample_main.xml
new file mode 100644
index 00000000..02697a00
--- /dev/null
+++ b/maoni-sample/src/main/res/layout/content_maoni_sample_main.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/maoni-sample/src/main/res/menu/menu_scrolling.xml b/maoni-sample/src/main/res/menu/menu_scrolling.xml
new file mode 100644
index 00000000..8a35e58a
--- /dev/null
+++ b/maoni-sample/src/main/res/menu/menu_scrolling.xml
@@ -0,0 +1,10 @@
+
diff --git a/maoni-sample/src/main/res/mipmap-hdpi/ic_launcher.png b/maoni-sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..cde69bcc
Binary files /dev/null and b/maoni-sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/maoni-sample/src/main/res/mipmap-mdpi/ic_launcher.png b/maoni-sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..c133a0cb
Binary files /dev/null and b/maoni-sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/maoni-sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/maoni-sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..bfa42f0e
Binary files /dev/null and b/maoni-sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/maoni-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/maoni-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..324e72cd
Binary files /dev/null and b/maoni-sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/maoni-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/maoni-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..aee44e13
Binary files /dev/null and b/maoni-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/maoni-sample/src/main/res/values-v21/styles.xml b/maoni-sample/src/main/res/values-v21/styles.xml
new file mode 100644
index 00000000..dbbdd40f
--- /dev/null
+++ b/maoni-sample/src/main/res/values-v21/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/maoni-sample/src/main/res/values-w820dp/dimens.xml b/maoni-sample/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 00000000..63fc8164
--- /dev/null
+++ b/maoni-sample/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/maoni-sample/src/main/res/values/colors.xml b/maoni-sample/src/main/res/values/colors.xml
new file mode 100644
index 00000000..3ab3e9cb
--- /dev/null
+++ b/maoni-sample/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/maoni-sample/src/main/res/values/dimens.xml b/maoni-sample/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..7b1b7d37
--- /dev/null
+++ b/maoni-sample/src/main/res/values/dimens.xml
@@ -0,0 +1,8 @@
+
+ 180dp
+ 16dp
+ 16dp
+
+ 16dp
+ 16dp
+
diff --git a/maoni-sample/src/main/res/values/strings.xml b/maoni-sample/src/main/res/values/strings.xml
new file mode 100644
index 00000000..4b811f91
--- /dev/null
+++ b/maoni-sample/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ Maoni Demo
+ Feedback
+ Maoni Demo
+
diff --git a/maoni-sample/src/main/res/values/styles.xml b/maoni-sample/src/main/res/values/styles.xml
new file mode 100644
index 00000000..545b9c6d
--- /dev/null
+++ b/maoni-sample/src/main/res/values/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maoni-sample/src/test/java/org/rm3l/maoni/ExampleUnitTest.java b/maoni-sample/src/test/java/org/rm3l/maoni/ExampleUnitTest.java
new file mode 100644
index 00000000..80c5fa1c
--- /dev/null
+++ b/maoni-sample/src/test/java/org/rm3l/maoni/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.rm3l.maoni;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/maoni/.gitignore b/maoni/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/maoni/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/maoni/build.gradle b/maoni/build.gradle
new file mode 100644
index 00000000..153c69c1
--- /dev/null
+++ b/maoni/build.gradle
@@ -0,0 +1,26 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.2"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ testCompile 'junit:junit:4.12'
+ compile 'com.android.support:appcompat-v7:23.3.0'
+ compile 'com.android.support:design:23.3.0'
+}
diff --git a/maoni/proguard-rules.pro b/maoni/proguard-rules.pro
new file mode 100644
index 00000000..88116af3
--- /dev/null
+++ b/maoni/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/rm3l/Library/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 *;
+#}
diff --git a/maoni/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java b/maoni/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java
new file mode 100644
index 00000000..acf3cb6a
--- /dev/null
+++ b/maoni/src/androidTest/java/org/rm3l/maoni/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.rm3l.maoni;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/maoni/src/main/AndroidManifest.xml b/maoni/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..41d24aed
--- /dev/null
+++ b/maoni/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/maoni/src/main/java/org/rm3l/maoni/Maoni.java b/maoni/src/main/java/org/rm3l/maoni/Maoni.java
new file mode 100644
index 00000000..1d5a3141
--- /dev/null
+++ b/maoni/src/main/java/org/rm3l/maoni/Maoni.java
@@ -0,0 +1,75 @@
+package org.rm3l.maoni;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.rm3l.maoni.contract.Validator;
+import org.rm3l.maoni.ui.MaoniActivity;
+import org.rm3l.maoni.utils.ViewUtils;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+
+/**
+ * TODO Created by rm3l on 05/05/16.
+ */
+public class Maoni {
+
+ private static final String LOG_TAG = Maoni.class.getSimpleName();
+ private final Class mConcreteActivityType;
+ @Nullable
+ private String windowTitle;
+ @Nullable
+ private String message;
+ private boolean withEmailField;
+ private boolean closeOnCallbackError;
+ @Nullable
+ private Validator validator;
+
+ public Maoni(@Nullable final Class concreteActivityType) {
+ this.mConcreteActivityType = concreteActivityType;
+ }
+
+ @Nullable
+ public String getWindowTitle() {
+ return windowTitle;
+ }
+
+ public Maoni windowTitle(@Nullable String windowTitle) {
+ this.windowTitle = windowTitle;
+ return this;
+ }
+
+ @Nullable
+ public String getMessage() {
+ return message;
+ }
+
+ public Maoni message(@Nullable String message) {
+ this.message = message;
+ return this;
+ }
+
+ public void start(@NonNull final Activity callerActivity) {
+ if (callerActivity == null) {
+ Toast.makeText(callerActivity, "Target activity is undefined - please try again later", Toast.LENGTH_SHORT).show();
+ Log.d(LOG_TAG, "Target activity is undefined");
+ return;
+ }
+
+ final Intent maoniIntent = new Intent(callerActivity, mConcreteActivityType);
+ final File screenshotFile = new File(callerActivity.getCacheDir(), "feedback_screenshot.png");
+ ViewUtils.exportViewToFile(callerActivity, callerActivity.getWindow().getDecorView(), screenshotFile);
+ maoniIntent.putExtra(MaoniActivity.SCREENSHOT_FILE, screenshotFile.getAbsolutePath());
+ maoniIntent.putExtra(MaoniActivity.CALLER_ACTIVITY, callerActivity.getClass().getCanonicalName());
+ //TODO Add parameters over here
+ callerActivity.startActivity(maoniIntent);
+ }
+
+}
diff --git a/maoni/src/main/java/org/rm3l/maoni/contract/Validator.java b/maoni/src/main/java/org/rm3l/maoni/contract/Validator.java
new file mode 100644
index 00000000..810428ff
--- /dev/null
+++ b/maoni/src/main/java/org/rm3l/maoni/contract/Validator.java
@@ -0,0 +1,12 @@
+package org.rm3l.maoni.contract;
+
+import android.support.annotation.NonNull;
+import android.view.View;
+
+/**
+ * Created by rm3l on 05/05/16.
+ */
+public interface Validator {
+
+ boolean validateForm(@NonNull final View rootView);
+}
diff --git a/maoni/src/main/java/org/rm3l/maoni/model/Feedback.java b/maoni/src/main/java/org/rm3l/maoni/model/Feedback.java
new file mode 100644
index 00000000..57187c64
--- /dev/null
+++ b/maoni/src/main/java/org/rm3l/maoni/model/Feedback.java
@@ -0,0 +1,122 @@
+package org.rm3l.maoni.model;
+
+import android.net.wifi.SupplicantState;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.io.File;
+
+/**
+ * Created by rm3l on 08/05/16.
+ */
+public class Feedback {
+
+ @NonNull
+ public final String id;
+
+ @NonNull
+ public final Phone phoneInfo;
+
+ @NonNull
+ public final App appInfo;
+
+ @NonNull
+ public final String userId;
+
+ @NonNull
+ public final String userComment;
+
+ public final boolean includeScreenshot;
+
+ @Nullable
+ public final File screenshotFilePath;
+
+ public Feedback(@NonNull String id,
+ @NonNull Phone phoneInfo,
+ @NonNull App appInfo,
+ @NonNull String userId,
+ @NonNull String userComment,
+ boolean includeScreenshot,
+ @Nullable String screenshotFilePath) {
+
+ this.id = id;
+ this.phoneInfo = phoneInfo;
+ this.appInfo = appInfo;
+ this.userId = userId;
+ this.userComment = userComment;
+ this.includeScreenshot = includeScreenshot;
+ this.screenshotFilePath =
+ (screenshotFilePath != null ? new File(screenshotFilePath) : null);
+ }
+
+ @NonNull
+ public String getId() {
+ return id;
+ }
+
+
+ public static class Phone {
+
+ public final String model;
+
+ public final String androidVersion;
+
+ public final SupplicantState wifiState;
+
+ public final boolean mobileDataEnabled;
+
+ public final boolean gpsEnabled;
+
+ public final String screenResolution;
+
+ public Phone(String model,
+ String androidVersion,
+ SupplicantState wifiState,
+ boolean mobileDataEnabled,
+ boolean gpsEnabled,
+ String screenResolution) {
+
+ this.model = model;
+ this.androidVersion = androidVersion;
+ this.wifiState = wifiState;
+ this.mobileDataEnabled = mobileDataEnabled;
+ this.gpsEnabled = gpsEnabled;
+ this.screenResolution = screenResolution;
+ }
+
+ }
+
+ public static class App {
+
+ public final String caller;
+
+ public final boolean debug;
+
+ public final String applicationId;
+
+ public final int versionCode;
+
+ public final String flavor;
+
+ public final String buildType;
+
+ public final String versionName;
+
+ public App(String caller, boolean debug,
+ String applicationId,
+ int versionCode,
+ String flavor,
+ String buildType,
+ String versionName) {
+
+ this.caller = caller;
+ this.debug = debug;
+ this.applicationId = applicationId;
+ this.versionCode = versionCode;
+ this.flavor = flavor;
+ this.buildType = buildType;
+ this.versionName = versionName;
+ }
+
+ }
+}
diff --git a/maoni/src/main/java/org/rm3l/maoni/ui/MaoniActivity.java b/maoni/src/main/java/org/rm3l/maoni/ui/MaoniActivity.java
new file mode 100644
index 00000000..8d6f4850
--- /dev/null
+++ b/maoni/src/main/java/org/rm3l/maoni/ui/MaoniActivity.java
@@ -0,0 +1,463 @@
+package org.rm3l.maoni.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.location.LocationManager;
+import android.net.ConnectivityManager;
+import android.net.wifi.SupplicantState;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.TextInputLayout;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import org.rm3l.maoni.BuildConfig;
+import org.rm3l.maoni.R;
+import org.rm3l.maoni.contract.Validator;
+import org.rm3l.maoni.model.Feedback;
+import org.rm3l.maoni.utils.ViewUtils;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.UUID;
+
+/**
+ * Created by rm3l on 05/05/16.
+ */
+public abstract class MaoniActivity extends AppCompatActivity implements Validator {
+
+ public static final String USER_IDNETIFIER = "UDER_IDENTIFIER";
+ public static final String SCREENSHOT_FILE = "SCREENSHOT_FILE";
+ public static final String CALLER_ACTIVITY = "CALLER_ACTIVITY";
+ private static final String LOG_TAG = MaoniActivity.class.getSimpleName();
+ private Bitmap mBitmap;
+
+ private TextInputLayout mEmailInputLayout;
+ private EditText mEmail;
+
+ private TextInputLayout mContentInputLayout;
+ private EditText mContent;
+
+ private CheckBox mIncludeScreenshotAndLogs;
+
+ private ImageButton mScreenshotThumb;
+ private ImageView mScreenshotExpanded;
+
+ private String mScreenshotFilePath;
+
+ // Hold a reference to the current animator,
+ // so that it can be canceled mid-way.
+ private Animator mCurrentAnimator;
+
+ // The system "short" animation time duration, in milliseconds. This
+ // duration is ideal for subtle animations or animations that occur
+ // very frequently.
+ private int mShortAnimationDuration;
+ private Toolbar mToolbar;
+
+ private String mFeedbackUniqueId;
+ private Feedback.App mAppInfo;
+ private Feedback.Phone mPhoneInfo;
+ private View mRootView;
+
+ @Override
+ protected final void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setTheme(R.style.AppTheme_NoActionBar);
+
+ setContentView(R.layout.maoni_activity_feedback);
+
+ mRootView = findViewById(R.id.maoni_container);
+
+ final Context applicationContext = getApplicationContext();
+
+ mToolbar = (Toolbar) findViewById(R.id.maoni_toolbar);
+ if (mToolbar != null) {
+ mToolbar.setTitle(R.string.send_feedback);
+ mToolbar.setTitleTextAppearance(applicationContext,
+ R.style.ToolbarTitle);
+ mToolbar.setSubtitleTextAppearance(applicationContext,
+ R.style.ToolbarSubtitle);
+ mToolbar.setTitleTextColor(ContextCompat.getColor(this,
+ R.color.white));
+ mToolbar.setSubtitleTextColor(ContextCompat.getColor(this,
+ R.color.white));
+ setSupportActionBar(mToolbar);
+ }
+
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+ }
+
+ mEmailInputLayout = (TextInputLayout) findViewById(R.id.maoni_email_input_layout);
+ mEmail = (EditText) findViewById(R.id.maoni_email);
+
+ mContentInputLayout = (TextInputLayout) findViewById(R.id.maoni_content_input_layout);
+ mContent = (EditText) findViewById(R.id.maoni_content);
+
+ mIncludeScreenshotAndLogs = (CheckBox) findViewById(R.id.maoni_include_screenshot_and_logs);
+
+ mScreenshotThumb = (ImageButton)
+ findViewById(R.id.maoni_include_screenshot_and_logs_content_screenshot);
+ mScreenshotExpanded = (ImageView)
+ findViewById(R.id.maoni_include_screenshot_and_logs_content_screenshot_expanded);
+
+ // Retrieve and cache the system's default "short" animation time.
+ mShortAnimationDuration = getResources().getInteger(
+ android.R.integer.config_shortAnimTime);
+
+ final Intent intent = getIntent();
+
+ //Set user-defined email if any
+ mEmail.setText(intent.getStringExtra(USER_IDNETIFIER));
+
+ final View screenshotAndLogsContentView =
+ findViewById(R.id.maoni_include_screenshot_and_logs_content);
+
+ mScreenshotFilePath = intent.getStringExtra(SCREENSHOT_FILE);
+ if (!TextUtils.isEmpty(mScreenshotFilePath)) {
+ final File file = new File(mScreenshotFilePath);
+ if (file.exists()) {
+ mIncludeScreenshotAndLogs.setVisibility(View.VISIBLE);
+ screenshotAndLogsContentView.setVisibility(View.VISIBLE);
+ mBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
+ mScreenshotThumb.setImageBitmap(mBitmap);
+ mScreenshotExpanded.setImageBitmap(mBitmap);
+
+ // Hook up clicks on the thumbnail views.
+
+ mScreenshotThumb.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ zoomImageFromThumb(mScreenshotThumb, mBitmap);
+ ViewUtils.hideToolbar(mToolbar);
+ }
+ });
+ } else {
+ mIncludeScreenshotAndLogs.setVisibility(View.GONE);
+ screenshotAndLogsContentView.setVisibility(View.GONE);
+ }
+ } else {
+ mIncludeScreenshotAndLogs.setVisibility(View.GONE);
+ screenshotAndLogsContentView.setVisibility(View.GONE);
+ }
+
+ mFeedbackUniqueId = UUID.randomUUID().toString();
+
+ findViewById(R.id.maoni_fab).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ validateAndSubmitForm();
+ }
+ });
+
+ setAppRelatedInfo();
+ setPhoneRelatedInfo();
+ }
+
+ private void setAppRelatedInfo() {
+
+ // Set app related properties
+ final PackageManager manager = getPackageManager();
+ PackageInfo info = null;
+ try {
+ info = manager.getPackageInfo(getPackageName(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ //No worries
+ }
+
+ final String callerActivity = getIntent().getStringExtra(CALLER_ACTIVITY);
+ mAppInfo = new Feedback.App(
+ callerActivity != null ? callerActivity : getClass().getSimpleName(),
+ BuildConfig.DEBUG,
+ BuildConfig.APPLICATION_ID,
+ info != null ? info.versionCode : BuildConfig.VERSION_CODE,
+ BuildConfig.FLAVOR,
+ BuildConfig.BUILD_TYPE,
+ info != null ? info.versionName : BuildConfig.VERSION_NAME);
+ }
+
+ private void setPhoneRelatedInfo() {
+
+ // Set phone related properties
+ SupplicantState supplicantState = null;
+ try {
+ final WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+ @SuppressWarnings("MissingPermission")
+ final WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ supplicantState = wifiInfo.getSupplicantState();
+ } catch (Exception e) {
+ //No worries
+ }
+
+ boolean mobileDataEnabled = false; // Assume disabled
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ try {
+ Class cmClass = Class.forName(cm.getClass().getName());
+ @SuppressWarnings("unchecked")
+ Method method = cmClass.getDeclaredMethod("getMobileDataEnabled");
+ method.setAccessible(true); // Make the method callable
+ // get the setting for "mobile data"
+ mobileDataEnabled = (Boolean) method.invoke(cm);
+ } catch (Exception e) {
+ // Some problem accessible private API
+ }
+ boolean gpsEnabled = false;
+ try {
+ final LocationManager manager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ gpsEnabled = manager.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ } catch (Exception e) {
+ //No worries
+ }
+
+ String resolution = null;
+ try {
+ DisplayMetrics metrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ resolution = Integer.toString(metrics.widthPixels) + "x" + Integer.toString(metrics.heightPixels);
+ } catch (Exception e) {
+ //No worries
+ }
+
+ mPhoneInfo = new Feedback.Phone(Build.MODEL,
+ Build.VERSION.RELEASE,
+ supplicantState,
+ mobileDataEnabled,
+ gpsEnabled,
+ resolution);
+ }
+
+ @Override
+ public final void onBackPressed() {
+ if (mScreenshotExpanded.getVisibility() == View.VISIBLE) {
+ //Unzoom
+ mScreenshotExpanded.performClick();
+ } else {
+ onDismiss();
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean validateForm(@NonNull View rootView) {
+ if (TextUtils.isEmpty(mEmail.getText())) {
+ mEmailInputLayout.setErrorEnabled(true);
+ mEmailInputLayout.setError("Must not be blank");
+ return false;
+ } else {
+ mEmailInputLayout.setErrorEnabled(false);
+ }
+ if (TextUtils.isEmpty(mContent.getText())) {
+ mContentInputLayout.setErrorEnabled(true);
+ mContentInputLayout.setError("Must not be blank");
+ return false;
+ } else {
+ mContentInputLayout.setErrorEnabled(false);
+ }
+ return true;
+ }
+
+ @Override
+ public final boolean onOptionsItemSelected(@NonNull MenuItem item) {
+
+ switch (item.getItemId()) {
+
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+
+ default:
+ break;
+ }
+ return true;
+ }
+
+ private void validateAndSubmitForm() {
+ //Validate form
+ if (this.validateForm(mRootView)) {
+ //Check that device is actually connected to the internet prior to going any further
+ final boolean includeScreenshot = mIncludeScreenshotAndLogs.isChecked();
+ final String emailText = mEmail.getText().toString();
+ final String contentText = mContent.getText().toString();
+
+ //Call actual implementation
+ final Feedback feedback =
+ new Feedback(mFeedbackUniqueId, mPhoneInfo, mAppInfo,
+ emailText, contentText, includeScreenshot, mScreenshotFilePath);
+ this.onSendButtonClicked(feedback);
+ } //else do nothing - this is up to the implementation
+ }
+
+ private void zoomImageFromThumb(final View thumbView, Bitmap bitmap) {
+ // If there's an animation in progress, cancel it
+ // immediately and proceed with this one.
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+
+ // Load the high-resolution "zoomed-in" image.
+ final ImageView expandedImageView = (ImageView) findViewById(
+ R.id.maoni_include_screenshot_and_logs_content_screenshot_expanded);
+ expandedImageView.setImageBitmap(bitmap);
+
+ // Calculate the starting and ending bounds for the zoomed-in image.
+ // This step involves lots of math. Yay, math.
+ final Rect startBounds = new Rect();
+ final Rect finalBounds = new Rect();
+ final Point globalOffset = new Point();
+
+ // The start bounds are the global visible rectangle of the thumbnail,
+ // and the final bounds are the global visible rectangle of the container
+ // view. Also set the container view's offset as the origin for the
+ // bounds, since that's the origin for the positioning animation
+ // properties (X, Y).
+ thumbView.getGlobalVisibleRect(startBounds);
+ findViewById(R.id.maoni_container)
+ .getGlobalVisibleRect(finalBounds, globalOffset);
+ startBounds.offset(-globalOffset.x, -globalOffset.y);
+ finalBounds.offset(-globalOffset.x, -globalOffset.y);
+
+ // Adjust the start bounds to be the same aspect ratio as the final
+ // bounds using the "center crop" technique. This prevents undesirable
+ // stretching during the animation. Also calculate the start scaling
+ // factor (the end scaling factor is always 1.0).
+ float startScale;
+ if ((float) finalBounds.width() / finalBounds.height()
+ > (float) startBounds.width() / startBounds.height()) {
+ // Extend start bounds horizontally
+ startScale = (float) startBounds.height() / finalBounds.height();
+ float startWidth = startScale * finalBounds.width();
+ float deltaWidth = (startWidth - startBounds.width()) / 2;
+ startBounds.left -= deltaWidth;
+ startBounds.right += deltaWidth;
+ } else {
+ // Extend start bounds vertically
+ startScale = (float) startBounds.width() / finalBounds.width();
+ float startHeight = startScale * finalBounds.height();
+ float deltaHeight = (startHeight - startBounds.height()) / 2;
+ startBounds.top -= deltaHeight;
+ startBounds.bottom += deltaHeight;
+ }
+
+ // Hide the thumbnail and show the zoomed-in view. When the animation
+ // begins, it will position the zoomed-in view in the place of the
+ // thumbnail.
+ thumbView.setAlpha(0f);
+ expandedImageView.setVisibility(View.VISIBLE);
+
+ // Set the pivot point for SCALE_X and SCALE_Y transformations
+ // to the top-left corner of the zoomed-in view (the default
+ // is the center of the view).
+ expandedImageView.setPivotX(0f);
+ expandedImageView.setPivotY(0f);
+
+ // Construct and run the parallel animation of the four translation and
+ // scale properties (X, Y, SCALE_X, and SCALE_Y).
+ AnimatorSet set = new AnimatorSet();
+ set
+ .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
+ startBounds.left, finalBounds.left))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
+ startBounds.top, finalBounds.top))
+ .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
+ startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,
+ View.SCALE_Y, startScale, 1f));
+ set.setDuration(mShortAnimationDuration);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mCurrentAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCurrentAnimator = null;
+ }
+ });
+ set.start();
+ mCurrentAnimator = set;
+
+ // Upon clicking the zoomed-in image, it should zoom back down
+ // to the original bounds and show the thumbnail instead of
+ // the expanded image.
+ final float startScaleFinal = startScale;
+ expandedImageView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+
+ // Animate the four positioning/sizing properties in parallel,
+ // back to their original values.
+ AnimatorSet set = new AnimatorSet();
+ set.play(ObjectAnimator
+ .ofFloat(expandedImageView, View.X, startBounds.left))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.Y, startBounds.top))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_X, startScaleFinal))
+ .with(ObjectAnimator
+ .ofFloat(expandedImageView,
+ View.SCALE_Y, startScaleFinal));
+ set.setDuration(mShortAnimationDuration);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ thumbView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ thumbView.setAlpha(1f);
+ expandedImageView.setVisibility(View.GONE);
+ mCurrentAnimator = null;
+ }
+ });
+ set.start();
+ mCurrentAnimator = set;
+
+ ViewUtils.showToolbar(mToolbar);
+ }
+ });
+ }
+
+ protected abstract void onDismiss();
+
+ protected abstract void onSendButtonClicked(@NonNull final Feedback feedback);
+
+}
diff --git a/maoni/src/main/java/org/rm3l/maoni/utils/ViewUtils.java b/maoni/src/main/java/org/rm3l/maoni/utils/ViewUtils.java
new file mode 100644
index 00000000..b48dfe2c
--- /dev/null
+++ b/maoni/src/main/java/org/rm3l/maoni/utils/ViewUtils.java
@@ -0,0 +1,94 @@
+package org.rm3l.maoni.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.Toolbar;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Created by rm3l on 08/05/16.
+ */
+public final class ViewUtils {
+
+ public static final int COMPRESSION_QUALITY = 100;
+ public static final int DEFAULT_BITMAP_WIDTH = 640;
+ public static final int DEFAULT_BITMAP_HEIGHT = 480;
+
+ private ViewUtils() {
+ }
+
+ public static void hideToolbar(@Nullable final Toolbar toolbar) {
+ if (toolbar == null) {
+ return;
+ }
+ toolbar.animate()
+ .translationY(-toolbar.getHeight())
+ .setInterpolator(new AccelerateInterpolator(2));
+ }
+
+ public static void showToolbar(@Nullable final Toolbar toolbar) {
+ if (toolbar == null) {
+ return;
+ }
+ toolbar.animate()
+ .translationY(0)
+ .setInterpolator(new DecelerateInterpolator(2));
+ }
+
+ @Nullable
+ public static Bitmap toBitmap(@Nullable final View view) {
+ if (view == null) {
+ return null;
+ }
+ final int width = view.getWidth();
+ final int height = view.getHeight();
+ final Bitmap bitmapToExport = Bitmap
+ .createBitmap(width > 0 ? width : DEFAULT_BITMAP_WIDTH,
+ height > 0 ? height : DEFAULT_BITMAP_HEIGHT,
+ Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmapToExport);
+ view.draw(canvas);
+ return bitmapToExport;
+ }
+
+ public static void exportViewToFile(@NonNull final Context context,
+ @NonNull final View view, @NonNull final File file) {
+ final Bitmap bitmap = toBitmap(view);
+ if (bitmap == null) {
+ return;
+ }
+ exportBitmapToFile(context, bitmap, file);
+ }
+
+ public static void exportBitmapToFile(@NonNull final Context context,
+ @NonNull final Bitmap bitmap, @NonNull final File file) {
+ OutputStream outputStream = null;
+ try {
+ outputStream = new BufferedOutputStream(new FileOutputStream(file, false));
+ bitmap.compress(Bitmap.CompressFormat.PNG, COMPRESSION_QUALITY, outputStream);
+ outputStream.flush();
+ } catch (final IOException e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ if (outputStream != null) {
+ outputStream.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ //No Worries
+ }
+ }
+ }
+}
diff --git a/maoni/src/main/res/drawable-hdpi/ic_feedback_white_24dp.png b/maoni/src/main/res/drawable-hdpi/ic_feedback_white_24dp.png
new file mode 100644
index 00000000..a34168c6
Binary files /dev/null and b/maoni/src/main/res/drawable-hdpi/ic_feedback_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-hdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-hdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..0f001720
Binary files /dev/null and b/maoni/src/main/res/drawable-hdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-ldrtl-hdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-ldrtl-hdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..0cc14e75
Binary files /dev/null and b/maoni/src/main/res/drawable-ldrtl-hdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-ldrtl-mdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-ldrtl-mdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..d5fe50b3
Binary files /dev/null and b/maoni/src/main/res/drawable-ldrtl-mdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-ldrtl-xhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-ldrtl-xhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..4735a7d7
Binary files /dev/null and b/maoni/src/main/res/drawable-ldrtl-xhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-ldrtl-xxhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-ldrtl-xxhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..9f64e5b3
Binary files /dev/null and b/maoni/src/main/res/drawable-ldrtl-xxhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-ldrtl-xxxhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-ldrtl-xxxhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..76d135b9
Binary files /dev/null and b/maoni/src/main/res/drawable-ldrtl-xxxhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-mdpi/ic_feedback_white_24dp.png b/maoni/src/main/res/drawable-mdpi/ic_feedback_white_24dp.png
new file mode 100644
index 00000000..389e9272
Binary files /dev/null and b/maoni/src/main/res/drawable-mdpi/ic_feedback_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-mdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-mdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..048d3eb2
Binary files /dev/null and b/maoni/src/main/res/drawable-mdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xhdpi/ic_feedback_white_24dp.png b/maoni/src/main/res/drawable-xhdpi/ic_feedback_white_24dp.png
new file mode 100644
index 00000000..0063b2f6
Binary files /dev/null and b/maoni/src/main/res/drawable-xhdpi/ic_feedback_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-xhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..ef59e776
Binary files /dev/null and b/maoni/src/main/res/drawable-xhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xxhdpi/ic_feedback_white_24dp.png b/maoni/src/main/res/drawable-xxhdpi/ic_feedback_white_24dp.png
new file mode 100644
index 00000000..25810dd7
Binary files /dev/null and b/maoni/src/main/res/drawable-xxhdpi/ic_feedback_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..1bc7552a
Binary files /dev/null and b/maoni/src/main/res/drawable-xxhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xxxhdpi/ic_feedback_white_24dp.png b/maoni/src/main/res/drawable-xxxhdpi/ic_feedback_white_24dp.png
new file mode 100644
index 00000000..cf4fd98a
Binary files /dev/null and b/maoni/src/main/res/drawable-xxxhdpi/ic_feedback_white_24dp.png differ
diff --git a/maoni/src/main/res/drawable-xxxhdpi/ic_send_white_24dp.png b/maoni/src/main/res/drawable-xxxhdpi/ic_send_white_24dp.png
new file mode 100644
index 00000000..6aeaa850
Binary files /dev/null and b/maoni/src/main/res/drawable-xxxhdpi/ic_send_white_24dp.png differ
diff --git a/maoni/src/main/res/layout/maoni_activity_feedback.xml b/maoni/src/main/res/layout/maoni_activity_feedback.xml
new file mode 100644
index 00000000..5ac6ff14
--- /dev/null
+++ b/maoni/src/main/res/layout/maoni_activity_feedback.xml
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maoni/src/main/res/layout/maoni_form_content.xml b/maoni/src/main/res/layout/maoni_form_content.xml
new file mode 100644
index 00000000..3d6a128e
--- /dev/null
+++ b/maoni/src/main/res/layout/maoni_form_content.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maoni/src/main/res/values-v21/styles.xml b/maoni/src/main/res/values-v21/styles.xml
new file mode 100644
index 00000000..dbbdd40f
--- /dev/null
+++ b/maoni/src/main/res/values-v21/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/maoni/src/main/res/values/colors.xml b/maoni/src/main/res/values/colors.xml
new file mode 100644
index 00000000..75a37f3a
--- /dev/null
+++ b/maoni/src/main/res/values/colors.xml
@@ -0,0 +1,32 @@
+
+
+
+
+ #FFFFFF
+
+ #000000
+ #00000000
+ #FF000000
+ #80000000
+
+
\ No newline at end of file
diff --git a/maoni/src/main/res/values/dimens.xml b/maoni/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..e174a2af
--- /dev/null
+++ b/maoni/src/main/res/values/dimens.xml
@@ -0,0 +1,10 @@
+
+
+
+ 16dp
+ 16dp
+
+ 180dp
+ 16dp
+ 16dp
+
\ No newline at end of file
diff --git a/maoni/src/main/res/values/strings.xml b/maoni/src/main/res/values/strings.xml
new file mode 100644
index 00000000..586d7bc6
--- /dev/null
+++ b/maoni/src/main/res/values/strings.xml
@@ -0,0 +1,13 @@
+
+ Maoni Library
+
+ Screenshot
+ Hey! Love or hate this app? Would you like to suggest a new feature, file a bug or simply say hello?\nWe would love to hear from you.
+ Your email address
+ Write your feedback here
+ Include screenshot
+ Touch to preview
+ The screenshot allows us to have more contextual information about your feedback, and will never ever be shared with third parties.
+ Send Feedback
+
+
diff --git a/maoni/src/main/res/values/styles.xml b/maoni/src/main/res/values/styles.xml
new file mode 100644
index 00000000..b6288129
--- /dev/null
+++ b/maoni/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/maoni/src/test/java/org/rm3l/maoni/ExampleUnitTest.java b/maoni/src/test/java/org/rm3l/maoni/ExampleUnitTest.java
new file mode 100644
index 00000000..80c5fa1c
--- /dev/null
+++ b/maoni/src/test/java/org/rm3l/maoni/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.rm3l.maoni;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..fe738ced
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':maoni-sample', ':maoni'