diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e5a96f6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.idea
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..e6b32bc
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/example/build.gradle b/example/build.gradle
new file mode 100644
index 0000000..618876e
--- /dev/null
+++ b/example/build.gradle
@@ -0,0 +1,29 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "com.topjohnwu.libsuexample"
+ minSdkVersion 16
+ targetSdkVersion 27
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation project(':superuser')
+}
diff --git a/example/proguard-rules.pro b/example/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/example/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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
diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3b5b598
--- /dev/null
+++ b/example/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/java/com/topjohnwu/libsuexample/ExampleApp.java b/example/src/main/java/com/topjohnwu/libsuexample/ExampleApp.java
new file mode 100644
index 0000000..eabc0b4
--- /dev/null
+++ b/example/src/main/java/com/topjohnwu/libsuexample/ExampleApp.java
@@ -0,0 +1,39 @@
+package com.topjohnwu.libsuexample;
+
+import android.app.Application;
+
+import com.topjohnwu.superuser.Shell;
+import com.topjohnwu.superuser.ShellContainer;
+
+/**
+ * The {@link Application} of the Example app.
+ *
+ * We implement {@link ShellContainer} to the {@link Application} of
+ * the app, which means that we would love our root shell to live as long as the
+ * application itself.
+ */
+public class ExampleApp extends Application implements ShellContainer {
+
+ /**
+ * A shell instance living alongside the lifecycle of {@link ExampleApp}
+ */
+ public Shell mShell;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ // Enable verbose logging flags
+ Shell.addFlags(Shell.FLAG_VERBOSE_LOGGING);
+ Shell.setGlobalContainer(this);
+ }
+
+ @Override
+ public Shell getShell() {
+ return mShell;
+ }
+
+ @Override
+ public void setShell(Shell shell) {
+ mShell = shell;
+ }
+}
diff --git a/example/src/main/java/com/topjohnwu/libsuexample/MainActivity.java b/example/src/main/java/com/topjohnwu/libsuexample/MainActivity.java
new file mode 100644
index 0000000..9ffe7e0
--- /dev/null
+++ b/example/src/main/java/com/topjohnwu/libsuexample/MainActivity.java
@@ -0,0 +1,75 @@
+package com.topjohnwu.libsuexample;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.topjohnwu.superuser.NoShellException;
+import com.topjohnwu.superuser.Shell;
+import com.topjohnwu.superuser.ShellCallback;
+import com.topjohnwu.superuser.ShellCallbackVector;
+
+import java.util.List;
+
+public class MainActivity extends Activity {
+
+ private TextView console;
+ private Button cmd, script, clear;
+ private EditText input;
+ private ScrollView sv;
+ private List callback;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ console = findViewById(R.id.console);
+ input = findViewById(R.id.cmd_input);
+ sv = findViewById(R.id.sv);
+
+ cmd = findViewById(R.id.run_cmd);
+ script = findViewById(R.id.run_script);
+ clear = findViewById(R.id.clear);
+
+ // Run the shell command in the input box
+ cmd.setOnClickListener(v -> {
+ Shell.su(callback, input.getText().toString());
+ input.setText("");
+ });
+
+ // Load a script from raw resources
+ script.setOnClickListener(v -> {
+ try {
+ Shell.getShell().loadInputStream(callback, callback,
+ getResources().openRawResource(R.raw.script));
+ } catch (NoShellException e) {
+ e.printStackTrace();
+ }
+ });
+
+ clear.setOnClickListener(v -> {
+ callback.clear();
+ console.setText("");
+ });
+
+ // We create a ShellCallback to update the UI with the Shell output
+ callback = new ShellCallback() {
+ StringBuilder builder = new StringBuilder();
+ @Override
+ public void onShellOutput(String e) {
+ builder.append(e).append('\n');
+ console.setText(builder);
+ sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
+ }
+
+ @Override
+ public void clear() {
+ builder = new StringBuilder();
+ }
+ };
+ }
+}
diff --git a/example/src/main/res/drawable-v24/ic_launcher_foreground.xml b/example/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/example/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/ic_launcher_background.xml b/example/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/example/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..8d222fd
--- /dev/null
+++ b/example/src/main/res/layout/activity_main.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher.png b/example/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher_round.png b/example/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher.png b/example/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher_round.png b/example/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/example/src/main/res/values/colors.xml b/example/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml
new file mode 100644
index 0000000..9cc8c05
--- /dev/null
+++ b/example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ LibSUExample
+
diff --git a/example/src/main/res/values/styles.xml b/example/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ff6c9d2
--- /dev/null
+++ b/example/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# 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.
+org.gradle.jvmargs=-Xmx1536m
+
+# 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
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..12b26ef
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jan 19 16:41:02 CST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /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 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..695c0e6
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':superuser', ':example'
diff --git a/superuser/.gitignore b/superuser/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/superuser/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/superuser/build.gradle b/superuser/build.gradle
new file mode 100644
index 0000000..dfea7d1
--- /dev/null
+++ b/superuser/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 27
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 27
+ versionCode 1
+ versionName "0.0.1"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+}
diff --git a/superuser/proguard-rules.pro b/superuser/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/superuser/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# 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
diff --git a/superuser/src/main/AndroidManifest.xml b/superuser/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..91f1013
--- /dev/null
+++ b/superuser/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/NoShellException.java b/superuser/src/main/java/com/topjohnwu/superuser/NoShellException.java
new file mode 100644
index 0000000..1ad2187
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/NoShellException.java
@@ -0,0 +1,12 @@
+package com.topjohnwu.superuser;
+
+/**
+ * Created by topjohnwu on 2018/1/19.
+ */
+
+public class NoShellException extends Exception {
+
+ public NoShellException() {
+ super("Unable to start a shell!");
+ }
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/Shell.java b/superuser/src/main/java/com/topjohnwu/superuser/Shell.java
new file mode 100644
index 0000000..f7f47ea
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/Shell.java
@@ -0,0 +1,281 @@
+package com.topjohnwu.superuser;
+
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Created by topjohnwu on 2018/1/19.
+ */
+
+public class Shell implements Closeable {
+
+ public static final int NOT_CHECKED = -1;
+ public static final int NON_ROOT_SHELL = 0;
+ public static final int ROOT_SHELL = 1;
+ public static final int ROOT_MOUNT_MASTER = 2;
+ public static final int FLAG_NON_ROOT_SHELL = 0x01;
+ public static final int FLAG_MOUNT_MASTER = 0x02;
+ public static final int FLAG_NO_GLOBAL_SHELL = 0x04;
+ public static final int FLAG_VERBOSE_LOGGING = 0x08;
+ public static final int FLAG_REDIRECT_STDERR = 0x10;
+
+ private static final String INTAG = "SHELL_IN";
+ private static final String TAG = "LIBSU";
+
+ static int flags = 0;
+ private static WeakReference weakContainer = new WeakReference<>(null);
+ final OutputStream STDIN;
+ final InputStream STDOUT;
+ final InputStream STDERR;
+ private final Process process;
+ public int status;
+
+ private Shell(String... cmd) throws IOException {
+ process = Runtime.getRuntime().exec(cmd);
+ STDIN = process.getOutputStream();
+ STDOUT = process.getInputStream();
+ STDERR = process.getErrorStream();
+ status = NOT_CHECKED;
+ }
+
+ public static void setGlobalContainer(ShellContainer container) {
+ weakContainer = new WeakReference<>(container);
+ }
+
+ public static void addFlags(int f) {
+ flags |= f;
+ }
+
+ public static void setFlags(int f) {
+ flags = f;
+ }
+
+ public static boolean rootAccess() {
+ try {
+ return getShell().status > 0;
+ } catch (NoShellException e) {
+ return false;
+ }
+ }
+
+ private static void testRootShell(Shell shell) throws IOException {
+ shell.STDIN.write(("id\n").getBytes("UTF-8"));
+ shell.STDIN.flush();
+ String s = new BufferedReader(new InputStreamReader(shell.STDOUT)).readLine();
+ if (TextUtils.isEmpty(s) || !s.contains("uid=0")) {
+ shell.STDIN.close();
+ shell.STDIN.close();
+ throw new IOException();
+ }
+ }
+
+ public static Shell getShell() throws NoShellException {
+ return getShell(Utils.hasFlag(FLAG_NO_GLOBAL_SHELL) ? null : weakContainer.get());
+ }
+
+ public static Shell getShell(ShellContainer container) throws NoShellException {
+ boolean newShell = container == null || container.getShell() == null;
+
+ Shell shell = newShell ? null : container.getShell();
+
+ if (!newShell) {
+ try {
+ shell.process.exitValue();
+ // Process is dead, start new shell
+ newShell = true;
+ } catch (IllegalThreadStateException ignored) {
+ // This should be the expected result
+ }
+ }
+
+ if (newShell && !Utils.hasFlag(FLAG_NON_ROOT_SHELL) && Utils.hasFlag(FLAG_MOUNT_MASTER)) {
+ // Try mount master
+ try {
+ shell = new Shell("su", "--mount-master");
+ testRootShell(shell);
+ Utils.log(TAG, "su --mount-master");
+ newShell = false;
+ shell.status = ROOT_MOUNT_MASTER;
+ } catch (IOException e) {
+ // Shell initialize failed
+ Utils.stackTrace(e);
+ }
+ }
+
+ if (newShell && !Utils.hasFlag(FLAG_NON_ROOT_SHELL)) {
+ // Try normal root shell
+ try {
+ shell = new Shell("su");
+ testRootShell(shell);
+ Utils.log(TAG, "su");
+ newShell = false;
+ shell.status = ROOT_SHELL;
+ } catch (IOException e) {
+ // Shell initialize failed
+ Utils.stackTrace(e);
+ }
+ }
+
+ if (newShell) {
+ // Try normal non-root shell
+ try {
+ shell = new Shell("sh");
+ Utils.log(TAG, "sh");
+ newShell = false;
+ shell.status = NON_ROOT_SHELL;
+ } catch (IOException e) {
+ // Shell initialize failed
+ Utils.stackTrace(e);
+ }
+ }
+
+ if (container != null)
+ container.setShell(shell);
+
+ return shell;
+ }
+
+ public static List sh(String... commands) {
+ List res = new ArrayList<>();
+ sh(res, commands);
+ return res;
+ }
+
+ public static void sh(Collection output, String... commands) {
+ try {
+ Shell shell = getShell();
+ shell.run(output, Utils.hasFlag(FLAG_REDIRECT_STDERR) ? output : null, commands);
+ } catch (NoShellException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public static void sh_async(String... commands) {
+ try {
+ Shell shell = getShell();
+ shell.run_async(commands);
+ } catch (NoShellException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static List su(String... commands) {
+ if (!rootAccess()) return sh();
+ return sh(commands);
+ }
+
+ public static void su(Collection output, String... commands) {
+ if (!rootAccess()) return;
+ sh(output, commands);
+ }
+
+ public static void su_async(String... commands) {
+ if (!rootAccess()) return;
+ sh_async(commands);
+ }
+
+ private void run_raw(boolean stdout, boolean stderr, String... commands) {
+ String suffix = (stdout ? "" : " >/dev/null") + (stderr ? "" : " 2>/dev/null") + "\n";
+ synchronized (process) {
+ try {
+ for (String command : commands) {
+ STDIN.write((command + suffix).getBytes("UTF-8"));
+ STDIN.flush();
+ Utils.log(INTAG, command);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ process.destroy();
+ }
+ }
+ }
+
+ private void run_sync_output(Collection output, Collection error, Runnable callback) {
+ CharSequence token = Utils.genRandomAlphaNumString(32);
+ StreamGobbler out, err = null;
+ synchronized (process) {
+ out = new StreamGobbler(STDOUT, output, token);
+ out.start();
+ if (error != null) {
+ err = new StreamGobbler(STDERR, error, token);
+ err.start();
+ }
+ callback.run();
+ try {
+ byte[] finalize = String.format("echo %s; echo %s >&2\n", token, token)
+ .getBytes("UTF-8");
+ STDIN.write(finalize);
+ STDIN.flush();
+ } catch (IOException e) {
+ process.destroy();
+ }
+ try {
+ out.join();
+ if (err != null)
+ err.join();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+
+ public void run(String... commands) {
+ run(null, null, commands);
+ }
+
+ public void run(Collection output, Collection error, String... commands) {
+ run_sync_output(output, error, () -> run_raw(output != null, error != null, commands));
+ }
+
+ public void run_async(String... commands) {
+ run_raw(false, false, commands);
+ }
+
+ public void loadInputStream(InputStream in) {
+ loadInputStream(null, null, in);
+ }
+
+ public void loadInputStream(Collection output, Collection error, InputStream in) {
+ run_sync_output(output, error, () -> {
+ StringBuilder builder = new StringBuilder();
+ try {
+ int read;
+ byte buffer[] = new byte[4096];
+ while ((read = in.read(buffer)) > 0)
+ builder.append(new String(buffer, 0, read));
+ STDIN.write(builder.toString().getBytes("UTF-8"));
+ STDIN.flush();
+ Utils.log(INTAG, builder);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ @Override
+ public void close() throws IOException {
+ STDIN.close();
+ STDERR.close();
+ STDOUT.close();
+ process.destroy();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ close();
+ }
+
+ interface IShellCallback {
+ void onShellOutput(String e);
+ }
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/ShellCallback.java b/superuser/src/main/java/com/topjohnwu/superuser/ShellCallback.java
new file mode 100644
index 0000000..2e51907
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/ShellCallback.java
@@ -0,0 +1,40 @@
+package com.topjohnwu.superuser;
+
+import android.os.Handler;
+
+import java.util.AbstractList;
+
+/**
+ * An {@link AbstractList} only used as an abstract container to call {@link #onShellOutput(String)}
+ * when a new output is added to the list.
+ *
+ * The method {@link #onShellOutput(String)} will be called in the thread where the
+ * {@link ShellCallbackVector} is constructed by using {@link Handler}. If you need to update
+ * the UI, please construct the list in the main thread.
+ */
+
+public abstract class ShellCallback extends AbstractList implements Shell.IShellCallback {
+
+ private Handler handler;
+
+ public ShellCallback() {
+ super();
+ handler = new Handler();
+ }
+
+ @Override
+ public final int size() {
+ return 0;
+ }
+
+ @Override
+ public synchronized boolean add(String s) {
+ handler.post(() -> onShellOutput(s));
+ return true;
+ }
+
+ @Override
+ public final String get(int index) {
+ return null;
+ }
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/ShellCallbackVector.java b/superuser/src/main/java/com/topjohnwu/superuser/ShellCallbackVector.java
new file mode 100644
index 0000000..b11ac22
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/ShellCallbackVector.java
@@ -0,0 +1,31 @@
+package com.topjohnwu.superuser;
+
+import android.os.Handler;
+
+import java.util.Vector;
+
+/**
+ * A {@link Vector} to store output of {@link Shell} and call {@link #onShellOutput(String)} when
+ * a new output is added to the list.
+ *
+ * The method {@link #onShellOutput(String)} will be called in the thread where the
+ * {@link ShellCallbackVector} is constructed by using {@link Handler}. If you need to update
+ * the UI, please construct the list in the main thread.
+ */
+
+public abstract class ShellCallbackVector extends Vector implements Shell.IShellCallback {
+
+ private Handler handler;
+
+ public ShellCallbackVector() {
+ super();
+ handler = new Handler();
+ }
+
+ @Override
+ public boolean add(String s) {
+ boolean ret = super.add(s);
+ handler.post(() -> onShellOutput(s));
+ return ret;
+ }
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/ShellContainer.java b/superuser/src/main/java/com/topjohnwu/superuser/ShellContainer.java
new file mode 100644
index 0000000..5dbf780
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/ShellContainer.java
@@ -0,0 +1,10 @@
+package com.topjohnwu.superuser;
+
+/**
+ * Created by topjohnwu on 2018/1/19.
+ */
+
+public interface ShellContainer {
+ Shell getShell();
+ void setShell(Shell shell);
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/StreamGobbler.java b/superuser/src/main/java/com/topjohnwu/superuser/StreamGobbler.java
new file mode 100644
index 0000000..8a34220
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/StreamGobbler.java
@@ -0,0 +1,50 @@
+package com.topjohnwu.superuser;
+
+import android.text.TextUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Created by topjohnwu on 2018/1/19.
+ */
+
+public class StreamGobbler extends Thread {
+
+ public static final String TAG = "SHELLOUT";
+
+ BufferedReader reader;
+ Collection writer;
+ CharSequence token;
+
+ public StreamGobbler(InputStream in, Collection out, CharSequence token) {
+ // Make sure our input is clean before running
+ try {
+ while (in.available() != 0)
+ in.skip(in.available());
+ } catch (IOException ignored) {}
+
+ reader = new BufferedReader(new InputStreamReader(in));
+ writer = out == null ? null : Collections.synchronizedCollection(out);
+ this.token = token;
+ }
+
+ @Override
+ public void run() {
+ // Keep reading the InputStream until it ends (or an error occurs)
+ try {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (TextUtils.equals(line, token))
+ return;
+ if (writer != null)
+ writer.add(line);
+ Utils.log(TAG, line);
+ }
+ } catch (IOException ignored) {}
+ }
+}
diff --git a/superuser/src/main/java/com/topjohnwu/superuser/Utils.java b/superuser/src/main/java/com/topjohnwu/superuser/Utils.java
new file mode 100644
index 0000000..253320f
--- /dev/null
+++ b/superuser/src/main/java/com/topjohnwu/superuser/Utils.java
@@ -0,0 +1,54 @@
+package com.topjohnwu.superuser;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+
+class Utils {
+
+ static final String LOWER_CASE = "abcdefghijklmnopqrstuvwxyz";
+ static final String UPPER_CASE = LOWER_CASE.toUpperCase();
+ static final String NUMBERS = "0123456789";
+ static final String ALPHANUM = LOWER_CASE + UPPER_CASE + NUMBERS;
+
+ static CharSequence genRandomAlphaNumString(int length) {
+ SecureRandom random = new SecureRandom();
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < length; ++i) {
+ builder.append(ALPHANUM.charAt(random.nextInt(ALPHANUM.length())));
+ }
+ return builder;
+ }
+
+ static boolean hasFlag(int flag) {
+ return hasFlag(Shell.flags, flag);
+ }
+
+ static boolean hasFlag(int flags, int flag) {
+ return (flags & flag) != 0;
+ }
+
+ static int inToOut(InputStream in, OutputStream out) throws IOException {
+ int read, total = 0;
+ byte buffer[] = new byte[4096];
+ while ((read = in.read(buffer)) > 0) {
+ out.write(buffer, 0, read);
+ total += read;
+ }
+ out.flush();
+ return total;
+ }
+
+ static void log(String tag, CharSequence log) {
+ if (hasFlag(Shell.FLAG_VERBOSE_LOGGING))
+ Log.d(tag, log.toString());
+ }
+
+ static void stackTrace(Throwable t) {
+ if (hasFlag(Shell.FLAG_VERBOSE_LOGGING))
+ t.printStackTrace();
+ }
+}
diff --git a/superuser/src/main/res/raw/script.sh b/superuser/src/main/res/raw/script.sh
new file mode 100644
index 0000000..54ed805
--- /dev/null
+++ b/superuser/src/main/res/raw/script.sh
@@ -0,0 +1,60 @@
+#!/system/bin/sh
+
+ui_print() {
+ echo "$1"
+}
+
+resolve_link() {
+ RESOLVED="$1"
+ while RESOLVE=`readlink $RESOLVED`; do
+ RESOLVED=$RESOLVE
+ done
+ echo $RESOLVED
+}
+
+is_mounted() {
+ TARGET="`resolve_link $1`"
+ cat /proc/mounts | grep " $TARGET " >/dev/null
+ return $?
+}
+
+find_boot_image() {
+ BOOTIMAGE=
+ if [ ! -z $SLOT ]; then
+ BOOTIMAGE=`find /dev/block -iname boot$SLOT | head -n 1` 2>/dev/null
+ fi
+ if [ -z "$BOOTIMAGE" ]; then
+ # The slot info is incorrect...
+ SLOT=
+ for BLOCK in boot_a kern-a android_boot kernel boot lnx bootimg; do
+ BOOTIMAGE=`find /dev/block -iname $BLOCK | head -n 1` 2>/dev/null
+ [ ! -z $BOOTIMAGE ] && break
+ done
+ fi
+ # Recovery fallback
+ if [ -z "$BOOTIMAGE" ]; then
+ for FSTAB in /etc/*fstab*; do
+ BOOTIMAGE=`grep -v '#' $FSTAB | grep -E '/boot[^a-zA-Z]' | grep -oE '/dev/[a-zA-Z0-9_./-]*'`
+ [ ! -z $BOOTIMAGE ] && break
+ done
+ fi
+ [ ! -z "$BOOTIMAGE" ] && BOOTIMAGE=`resolve_link $BOOTIMAGE`
+}
+
+# Check A/B slot
+SLOT=`getprop ro.boot.slot_suffix`
+if [ -z $SLOT ]; then
+ SLOT=_`getprop ro.boot.slot`
+ [ $SLOT = "_" ] && SLOT=
+fi
+
+# Check the boot image to make sure the slot actually make sense
+find_boot_image
+ui_print "- Found boot image: $BOOTIMAGE"
+[ -z $SLOT ] || ui_print "- A/B partition detected, current slot: $SLOT"
+
+cat /proc/mounts | grep -E '/dev/root|/system_root' >/dev/null && SKIP_INITRAMFS=true || SKIP_INITRAMFS=false
+if [ -f /system/init.rc ]; then
+SKIP_INITRAMFS=true
+fi
+$SKIP_INITRAMFS && ui_print "- Device skip_initramfs detected"